agent-quality-kit 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -12
- package/README.ru.md +185 -27
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +188 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/color-from-token/check.sh +5 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +92 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +25 -4
- package/package.json +3 -6
- package/tool/commands/context.mjs +37 -6
- package/tool/commands/doctor.mjs +139 -16
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/project.mjs +18 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/vitals.mjs +167 -0
- package/tool/i18n/en-docs.mjs +48 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +26 -279
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +48 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +26 -278
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +3 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +146 -15
- package/tool/lib/prove.mjs +11 -1
- package/tool/lib/repo.mjs +43 -3
- package/tool/program.mjs +33 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +528 -6
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +3 -1
- package/tool/selfcheck/units-level.mjs +147 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +164 -0
- package/tool/selfcheck/units-vitals.mjs +81 -0
- package/tool/selfcheck/units.mjs +3 -75
package/tool/i18n/ru.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { ruDocs } from "./ru-docs.mjs";
|
|
|
8
8
|
|
|
9
9
|
import { templates } from "./templates-ru.mjs";
|
|
10
10
|
|
|
11
|
+
import { ruGates } from "./ru-gates.mjs";
|
|
12
|
+
|
|
11
13
|
export const ru = {
|
|
12
14
|
learn: {
|
|
13
15
|
title: "Сказано вслух и не записано",
|
|
@@ -29,6 +31,7 @@ export const ru = {
|
|
|
29
31
|
init: "разложить правила и методички в текущий проект",
|
|
30
32
|
initForce: "перезаписать уже существующие файлы",
|
|
31
33
|
start: "кода ещё нет: сторожа дня 0 и порядок работы",
|
|
34
|
+
probe: "чего объявленные проверки не видят: подсадка брака в горячие по истории файлы",
|
|
32
35
|
prove: "доказать, что гейты ловят брак: каждый по своему красному и зелёному образцу",
|
|
33
36
|
doctor: "проверить, что разложено и чего не хватает",
|
|
34
37
|
doctorRun: "ещё и запустить объявленные гейты",
|
|
@@ -45,6 +48,7 @@ export const ru = {
|
|
|
45
48
|
contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
|
|
46
49
|
report: "обязательная форма отчёта: что стоит, что нет, что не прочитано; --since <ссылка> — ещё и чем доказан диф",
|
|
47
50
|
badge: "значок уровня для README — и проверка, что он не врёт",
|
|
51
|
+
version: "заставка и номер версии — то же по --version и -v",
|
|
48
52
|
noInstall: "Без установки: npx agent-quality-kit init",
|
|
49
53
|
language: "Язык вывода: AQK_LANG=en (или ru), иначе по системной локали",
|
|
50
54
|
},
|
|
@@ -88,6 +92,16 @@ export const ru = {
|
|
|
88
92
|
hasThings: "есть",
|
|
89
93
|
install: (cmd) => `поставить: ${cmd}`,
|
|
90
94
|
notApplicable: (n) => `Не применимо к этому репозиторию (${n}):`,
|
|
95
|
+
noBrowserServer:
|
|
96
|
+
"у проекта есть интерфейс, а у агента нет браузера: посмотреть на своё изменение глазами\n он не может и судит о работе по тому, что собралось. Это разные утверждения.",
|
|
97
|
+
noBrowserServerHow: (l) => `подключается MCP-сервером, любым агентом: ${l}`,
|
|
98
|
+
coveredBy: (n) => `закрыто другим арбитром: ${n} — переносимую запись ставить незачем`,
|
|
99
|
+
coveredByGate: (g) => `держит «${g}», объявленный в манифесте`,
|
|
100
|
+
coversUnknown: (l) => `covers называет гейт, которого нет в gates: ${l} — эти записи не закрыты ничем`,
|
|
101
|
+
coversUnproven: (e, g, codes) =>
|
|
102
|
+
`заявка не подтверждена: «${e}» объявлена закрытой гейтом «${g}», но ни его команда, ни\n конфиг линтера не называют правил ${codes} — запись может быть не закрыта ничем`,
|
|
103
|
+
coversUnprovenHow: (cmd) => `подтверди: добавь эти правила в линтер, либо поставь запись — ${cmd}`,
|
|
104
|
+
totalCovered: (n) => `закрыто другим арбитром ${n}`,
|
|
91
105
|
total: "Итого:",
|
|
92
106
|
totalHeld: (n) => `держит машина ${n}`,
|
|
93
107
|
totalTodo: (n) => `применимо но не поставлено ${n}`,
|
|
@@ -97,6 +111,7 @@ export const ru = {
|
|
|
97
111
|
sinceBadRef: (ref) => `не могу сравнить с «${ref}»: такой ссылки нет или это не репозиторий git`,
|
|
98
112
|
notScopable: "вывод без путей — дифом не сужается, оставлен красным",
|
|
99
113
|
outsideDiff: (n) => `находки есть, но вне дифа (${n})`,
|
|
114
|
+
advisoryQuiet: "(совещательный — уронить прогон не может)",
|
|
100
115
|
advisoryMark: "совещательный — показано, прогон не уронен",
|
|
101
116
|
advisorySummary: (names) =>
|
|
102
117
|
`совещательные и красные: ${names.join(", ")}. Это выключенные проверки: ` +
|
|
@@ -111,7 +126,15 @@ export const ru = {
|
|
|
111
126
|
manifestUnknown: (keys) =>
|
|
112
127
|
`В манифесте поля, которых стандарт не знает: ${keys.join(", ")}. Похоже на опечатку — ` +
|
|
113
128
|
`такое поле молча читается как отсутствующее, и вердикт выходит неверным.`,
|
|
129
|
+
manifestUnparsed: (n, t) => `строка ${n} манифеста не разобрана и НЕ ДЕЙСТВУЕТ: ${t}`,
|
|
130
|
+
manifestUnparsedWhy:
|
|
131
|
+
"имена полей и гейтов пишутся латиницей, цифрами, дефисом и подчёркиванием. Объявленное\n здесь не выполняется вовсе — а выглядит так, будто выполняется.",
|
|
114
132
|
manifestKnown: (keys) => `Поля манифеста: ${keys.join(", ")}`,
|
|
133
|
+
runVerdictOk: "Всё объявленное зелёное.",
|
|
134
|
+
runVerdictFail: (why) => `Прогон красный: ${why}.`,
|
|
135
|
+
whyMissing: "не хватает файлов из шапки",
|
|
136
|
+
whyLevel: "ступень не взята",
|
|
137
|
+
whyGates: (n, names) => `красных гейтов ${n}${names ? ` (${names})` : ""}`,
|
|
115
138
|
thresholdPass: (min) => `Порог AQK-${min} пройден.`,
|
|
116
139
|
thresholdFail: (min, now) => `Порог AQK-${min} НЕ пройден: сейчас AQK-${now}.`,
|
|
117
140
|
// Ступень взята, но прогон красный — это другое утверждение, и виновника называем сразу:
|
|
@@ -170,6 +193,8 @@ export const ru = {
|
|
|
170
193
|
has_agent_config: ["агента здесь не настраивали", "настройки агента есть"],
|
|
171
194
|
has_agent_entry: ["свода для агента здесь нет", "свод для агента есть"],
|
|
172
195
|
has_ui: ["не видно стилей и компонентов интерфейса", "интерфейс есть: стили или компоненты"],
|
|
196
|
+
has_mcp: ["агенту здесь не подключали внешних инструментов через MCP", "MCP-серверы объявлены"],
|
|
197
|
+
has_api_spec: ["не видно спецификации API", "спецификация API есть"],
|
|
173
198
|
},
|
|
174
199
|
},
|
|
175
200
|
|
|
@@ -196,282 +221,5 @@ export const ru = {
|
|
|
196
221
|
alreadyDeclared: "уже объявлен",
|
|
197
222
|
},
|
|
198
223
|
|
|
199
|
-
|
|
200
|
-
noSuchGate: (slug, cmd) => `Нет такого гейта: ${slug}\nСписок применимых — ${cmd}`,
|
|
201
|
-
toolMissing: (slug, missing) =>
|
|
202
|
-
`Записи ${slug} нужен ${missing.join(" и ")}, а его нет на этой машине.\n почини: поставь ${missing.join(" и ")} и повтори — запись делегирует готовому инструменту по устройству, своей проверки у неё нет.`,
|
|
203
|
-
noRecipe: (slug, stack, missing) =>
|
|
204
|
-
`У записи ${slug} нет команды ни под ${stack}, ни общей.` +
|
|
205
|
-
(Array.isArray(missing) && missing.length
|
|
206
|
-
? `\n почини: поставь ${missing.join(" или ")} и повтори — запись делегирует готовому инструменту по устройству, своей проверки у неё нет.`
|
|
207
|
-
: `\n почини: добавь рецепт под свой стек в gate.yml этой записи либо возьми другую.`),
|
|
208
|
-
thisStack: "этот стек",
|
|
209
|
-
needName: (cmd, doctor) => `Укажи имя гейта: ${cmd}. Список — ${doctor}`,
|
|
210
|
-
noManifest: (cmd) => `Нет .aqk.yml — сначала ${cmd}`,
|
|
211
|
-
notApplicable: (why) => `Этот гейт к репозиторию не применим: ${why}`,
|
|
212
|
-
installAnyway: "Ставлю всё равно — решение твоё, но сторожить ему нечего.",
|
|
213
|
-
copied: (n) => `${n} файлов: проверка и образцы`,
|
|
214
|
-
declared: (cmd) => `гейт объявлен: ${cmd}`,
|
|
215
|
-
notDeclared: (why, slug, cmd) => `не тронут (${why}). Впиши сам: ${slug}: "${cmd}"`,
|
|
216
|
-
nextTitle: "Дальше:",
|
|
217
|
-
next1: "Проверь, что он краснеет и молчит там, где должен:",
|
|
218
|
-
expectFail: "→ ожидается отказ",
|
|
219
|
-
expectSilence: "→ ожидается тишина",
|
|
220
|
-
next2: "Впиши команду в хук коммита и в конвейер.",
|
|
221
|
-
next2Why: "Гейт, который никто не запускает, — не гейт.",
|
|
222
|
-
next3: (cmd) => `Прогон всех объявленных: ${cmd}`,
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
gnew: {
|
|
226
|
-
needName: (cmd) => `Укажи имя: ${cmd}`,
|
|
227
|
-
badName: (slug) => `Имя «${slug}» не годится: латиница через дефис, например secrets-not-in-code.\nИмя читают в чужих проектах — оно часть словаря.`,
|
|
228
|
-
looksExisting: (slug) => `Похоже, такое уже есть: ${slug}`,
|
|
229
|
-
recipeNotGate: "Рецепт под другой стек — это строка в recipes существующей записи.",
|
|
230
|
-
forceHint: (cmd) => `Всё равно завести новую: ${cmd}`,
|
|
231
|
-
exists: (path) => `${path} уже существует.`,
|
|
232
|
-
nextTitle: "Дальше — по порядку:",
|
|
233
|
-
n1: "Проверь, нет ли готового правила",
|
|
234
|
-
n1Where: "в ruff, eslint, semgrep.",
|
|
235
|
-
n1Why: "Готовое точнее, подробнее и его поддерживают без тебя. Своя проверка — запасная.",
|
|
236
|
-
n2: "Положи образцы.",
|
|
237
|
-
n2Red: "— код, на котором проверка обязана сработать.",
|
|
238
|
-
n2Green: "— тот же код, но правильный.",
|
|
239
|
-
n2Why: "Зелёный важнее: он ловит проверку, которая краснеет на исправном коде.",
|
|
240
|
-
n3: "Напиши проверку",
|
|
241
|
-
n3Where: "в check.sh. В тексте отказа — что именно сделать.",
|
|
242
|
-
n4: "Заполни gate.yml:",
|
|
243
|
-
n4What: "намерение, триггер, доказательство отказом.",
|
|
244
|
-
n5: "Прогони:",
|
|
245
|
-
n5Why: "Арбитр обязан покраснеть на red/ и промолчать на green/. Не прошло — не запись.",
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
ratchet: {
|
|
249
|
-
needName: (cmd) => `Укажи гейт: ${cmd}. Он должен быть уже объявлен в .aqk.yml`,
|
|
250
|
-
noManifest: (cmd) => `Нет .aqk.yml — сначала ${cmd}`,
|
|
251
|
-
notDeclared: (slug, cmd) => `Гейт «${slug}» не объявлен в .aqk.yml. Сначала: ${cmd}`,
|
|
252
|
-
already: (slug) => `На гейте «${slug}» храповик уже стоит.`,
|
|
253
|
-
notRunnable: (slug, cmd) =>
|
|
254
|
-
`Гейт «${slug}» не запускается: ${cmd}\n` +
|
|
255
|
-
`Снимать долг с несуществующего сторожа нельзя — в реестр попадут его же сообщения\n` +
|
|
256
|
-
`об ошибке, и он станет разрешением. Сначала почини команду.`,
|
|
257
|
-
registryHead: (slug, stamp) =>
|
|
258
|
-
`# Реестр долга: ${slug}\n` +
|
|
259
|
-
`# Снят ${stamp}. Список разрешается ТОЛЬКО укорачивать.\n` +
|
|
260
|
-
`# Новое нарушение красит гейт; исправленное вычёркивается автоматически.\n` +
|
|
261
|
-
`#\n` +
|
|
262
|
-
`# Долг без цели и срока не кончается. Цель — сколько нарушений считать погашенным долгом;\n` +
|
|
263
|
-
`# по достижении храповик скажет убрать обёртку. Срок необязателен, но если он есть,\n` +
|
|
264
|
-
`# после этой даты непогашенный долг красит гейт — срок без последствия не срок.\n` +
|
|
265
|
-
`# aqk-goal: 0\n` +
|
|
266
|
-
`# aqk-deadline:\n`,
|
|
267
|
-
recorded: (n) => `${n} нарушений записано долгом`,
|
|
268
|
-
libCopied: "обёртка скопирована в проект",
|
|
269
|
-
wrapped: "команда завёрнута в храповик",
|
|
270
|
-
changesTitle: "Что это меняет:",
|
|
271
|
-
fromToday: "со дня установки",
|
|
272
|
-
changes1: (fromToday) => `Правило действует ${fromToday}. Старый код трогать не надо, но новое`,
|
|
273
|
-
changes2: "нарушение того же класса гейт не пропустит.",
|
|
274
|
-
test1: "Проверка, что это храповик, а не советчик: «может ли новый код добавить нарушение",
|
|
275
|
-
test2: "и пройти?» Может — значит гейта нет.",
|
|
276
|
-
run: (cmd) => `Прогнать: ${cmd}`,
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
find: {
|
|
280
|
-
needQuery: (cmd) => `Опиши намерение словами: ${cmd}`,
|
|
281
|
-
example: 'find "отладочная печать не доезжает до прода"',
|
|
282
|
-
exists: "Такое уже есть — новую запись заводить не надо:",
|
|
283
|
-
match: (pct) => `совпадение ${pct}%`,
|
|
284
|
-
recipes: (langs) => `рецепты: ${langs}общий`,
|
|
285
|
-
existsWhy1: "Если у тебя рецепт под другой стек — это строка в recipes существующей",
|
|
286
|
-
existsWhy2: "записи, а не новый гейт. Намерение одно, исполнителей может быть много.",
|
|
287
|
-
near: "Точного совпадения нет, но рядом лежит:",
|
|
288
|
-
nearWhy: "Прочитай их README. Если намерение то же — дополняй, а не заводи новое.",
|
|
289
|
-
none: "Такого намерения в каталоге нет.",
|
|
290
|
-
journal: "В журнале есть шишка на эту тему:",
|
|
291
|
-
journalWhy: "Шишка записана — значит доказательство для новой записи уже есть.",
|
|
292
|
-
howTitle: "Как добавить свой гейт:",
|
|
293
|
-
how1: "Назови отказ.",
|
|
294
|
-
how1What: "Какой конкретный брак он поймал в живом проекте, чего это стоило.",
|
|
295
|
-
how1Why: "«Это хорошая практика» не принимается: так каталог набирает сотни пунктов и умирает.",
|
|
296
|
-
how2: "Заведи папку",
|
|
297
|
-
how2What: "— gate.yml, red/, green/, README.md.",
|
|
298
|
-
how2Why: "Норма записи со всеми полями — kit/gates/README.md",
|
|
299
|
-
how3: "Проверь машиной:",
|
|
300
|
-
how3Why: "Арбитр обязан покраснеть на red/ и промолчать на green/. Не прошло — не запись.",
|
|
301
|
-
how4: "Пришли изменением",
|
|
302
|
-
how4What: "в репозиторий комплекта.",
|
|
303
|
-
},
|
|
304
|
-
|
|
305
|
-
why: {
|
|
306
|
-
needQuery: (cmd) => `Опиши, что пропустили: ${cmd}`,
|
|
307
|
-
example: 'why "файл вырос до девяти тысяч строк"',
|
|
308
|
-
ciAtOnce: "разом: doctor --run",
|
|
309
|
-
ciOwnStep: "отдельным шагом",
|
|
310
|
-
unsure: "Уверенного совпадения нет. Похоже на эти записи:",
|
|
311
|
-
unsureByName: (cmd) => `Назови запись именем: ${cmd}`,
|
|
312
|
-
unsureNone: (cmd) => `Ни одна не подходит — значит сторожа не было: ${cmd}`,
|
|
313
|
-
decideTitle: "Дальше решаешь ты:",
|
|
314
|
-
decideQ: "это твоя частность или общий случай?",
|
|
315
|
-
decideWhy: "Общий — идёт в каталог и достаётся всем. Частный — остаётся у тебя.",
|
|
316
|
-
decideNote: (cmd) => `Урок в общий журнал в любом случае: ${cmd}`,
|
|
317
|
-
fix: "Почини так:",
|
|
318
|
-
noGuard: "Сторожа не было.",
|
|
319
|
-
noGuardWhy: "В каталоге нет записи с таким намерением.",
|
|
320
|
-
noGuardFix: (cmd) => `заведи запись — ${cmd}`,
|
|
321
|
-
noGuardHint: "Красный образец бери прямо из этой поломки: она уже случилась, выдумывать нечего.",
|
|
322
|
-
closest: (slug, pct) => `Ближайшая запись каталога: ${slug} совпадение ${pct}%`,
|
|
323
|
-
notInstalled: "Сторож есть в каталоге, но в этом проекте не поставлен.",
|
|
324
|
-
notInstalledHint1: "Он покраснеет на старом коде — это нормально: старое закрывается храповиком,",
|
|
325
|
-
notInstalledHint2: (cmd) => `новое ловится со дня установки. ${cmd}`,
|
|
326
|
-
declaredAs: (cmd) => `Объявлен: ${cmd}`,
|
|
327
|
-
notRunning: "Сторож объявлен, но не запускается.",
|
|
328
|
-
notRunningWhy: "Худший случай: тишина читается как успех.",
|
|
329
|
-
notRunningFix: "путь или программа из команды не существуют — проверь их.",
|
|
330
|
-
notRunningHint: "Отсутствие сигнала неотличимо от успеха, поэтому это не «мелочь в конфиге».",
|
|
331
|
-
bypassed: "Сторож есть и эту поломку ловит — значит его обошли.",
|
|
332
|
-
noCiFix: "конвейера нет. Проверка, которую гоняет только человек,",
|
|
333
|
-
noCiHint: "работает ровно до первого «забыл».",
|
|
334
|
-
notInCiFix: "конвейер есть, но этот гейт в нём не запускается.",
|
|
335
|
-
notInCiHint: (cmd) => `Дешевле всего одним шагом: ${cmd} — он гоняет всё объявленное.`,
|
|
336
|
-
inCiFix: (how) => `конвейер его гоняет (${how}) — значит красный прогон`,
|
|
337
|
-
inCiHint1: "кто-то пропустил или обошёл. Перенеси правило из текста в механику:",
|
|
338
|
-
inCiHint2: "блокирующий шаг, а не необязательный; запрет слияния при красном.",
|
|
339
|
-
blind: "Сторож есть, стоит и запускается — но этой поломки не видит.",
|
|
340
|
-
blindFix: (dir) => `положи в ${dir} кусок кода из этой поломки`,
|
|
341
|
-
blindHint1: "и доведи проверку до красного на нём. Порядок обратный привычному: сначала",
|
|
342
|
-
blindHint2: "образец, потом правка — иначе непонятно, что именно починено.",
|
|
343
|
-
blindCheck: "Проверить после правки: bash tool/selfcheck/gates.sh",
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
init: {
|
|
347
|
-
noDocs: (dir) => `Не найден корпус методичек: ${dir}\nПохоже, пакет установлен не полностью.`,
|
|
348
|
-
docsRu:
|
|
349
|
-
"методички в .aqk/docs/ остаются на русском — решение, а не недоделка.",
|
|
350
|
-
created: (n) => `создано (${n}):`,
|
|
351
|
-
andMore: (n) => `… и ещё ${n}`,
|
|
352
|
-
kept: (n) => `уже были на месте, не тронуты (${n}):`,
|
|
353
|
-
overwrite: (cmd) => `перезаписать: ${cmd}`,
|
|
354
|
-
nextTitle: "Что дальше — по порядку:",
|
|
355
|
-
n1a: "Открой",
|
|
356
|
-
n1b: "и заполни раздел «Команды». Команда, которую нельзя",
|
|
357
|
-
n1c: "скопировать и выполнить, — не команда, а пожелание.",
|
|
358
|
-
n2a: "Прочитай",
|
|
359
|
-
n2b: "— это обязательный минимум",
|
|
360
|
-
n2c: "проекта без привязки к языку. Пройди сверху вниз и отметь, чего нет.",
|
|
361
|
-
n3a: "Заполни",
|
|
362
|
-
n3b: "— гейты, образцы, журнал. Уровень соответствия AQK",
|
|
363
|
-
n3c: (cmd) => `считается по нему: ${cmd}.`,
|
|
364
|
-
n4a: "Поднимайся по ступеням",
|
|
365
|
-
n4b: "по одной",
|
|
366
|
-
n4c: ". Гейт стережёт существующий артефакт:",
|
|
367
|
-
n4d: "проверка на код, которого ещё нет, — мёртвое правило.",
|
|
368
|
-
burned: (cmd) => `Обжёгся на чём-то — запиши: ${cmd}`,
|
|
369
|
-
hookHint: (cmd) => `Claude Code: ${cmd} — состояние проекта будет попадать в контекст агента\n само, до первого действия. Для других агентов хука нет, и это не недоделка.`,
|
|
370
|
-
},
|
|
371
|
-
|
|
372
|
-
feedback: {
|
|
373
|
-
title: "Если пригодилось:",
|
|
374
|
-
star: (url) => `Поставь звезду — ${url}`,
|
|
375
|
-
issue: "Нашёл баг или не подошло — заведи Issue, самая полезная обратная связь: и то и другое.",
|
|
376
|
-
once: "Это разовое сообщение: больше не покажется на этой машине.",
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
note: {
|
|
380
|
-
journalTitle: "Журнал шишек",
|
|
381
|
-
needTitle: (cmd) => `Нужен заголовок: ${cmd}`,
|
|
382
|
-
noJournal: (url) => `Клон журнала не найден.\nСделай один раз:\n git clone ${url}.git ~/projects/aqk\nили укажи путь: export AQK_HOME=/путь/к/aqk`,
|
|
383
|
-
journalMissing: (path) => `Журнал не найден: ${path}`,
|
|
384
|
-
emptyBody: `Тело записи пустое. Передай его на стандартный ввод, например:\n\n aqk note "заголовок" <<'EOF'\n **Что случилось.** ...\n **Чем это стоило.** ...\n **Вывод.** 🔧 ...\n EOF`,
|
|
385
|
-
noOutcome:
|
|
386
|
-
"В записи нет отметки решения. Урок без вывода — это история, а не урок.\n" +
|
|
387
|
-
"Припиши одну из трёх:\n" +
|
|
388
|
-
" ✅ **Стало гейтом:** <имя записи каталога>\n" +
|
|
389
|
-
" 🔧 **Стало правкой оснастки:** <что именно изменено>\n" +
|
|
390
|
-
" 👤 **Гейтом не станет:** <почему>",
|
|
391
|
-
unknownProject: "неизвестно",
|
|
392
|
-
projectField: "Проект",
|
|
393
|
-
pushed: (title) => `Записано и отправлено: ${title}`,
|
|
394
|
-
localOnly: (cmd) => `Записано локально, push не прошёл. Отправить: ${cmd}`,
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
blob: {
|
|
398
|
-
noDocs: (dir) => `Не найдены методички: ${dir}`,
|
|
399
|
-
header: (stamp) =>
|
|
400
|
-
`<!-- СОБРАНО КОМАНДОЙ aqk blob ${stamp} из kit/docs. Не править руками:\n` +
|
|
401
|
-
` правки затрёт следующая сборка. Источник — отдельные файлы. -->\n\n` +
|
|
402
|
-
`# AQK — методички одним файлом\n`,
|
|
403
|
-
source: (path) => `источник: ${path}`,
|
|
404
|
-
done: (n, kb) => `GOD_AI.md — ${n} файлов, ${kb} КБ`,
|
|
405
|
-
rebuilt: "Собирается заново каждой командой. Править надо оригиналы в kit/docs.",
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
start: {
|
|
409
|
-
noRecipeHere: "нужен инструмент, которого нет на этой машине",
|
|
410
|
-
initFailed: (cmd) => `Не получилось разложить комплект. Начни с ${cmd}`,
|
|
411
|
-
tooManyFiles: (n) => `В репозитории уже ${n} файлов кода — это другой сценарий.`,
|
|
412
|
-
useDoctor: (cmd) => `${cmd} осмотрит, что есть, и разделит записи на три списка:`,
|
|
413
|
-
threeLists: "держит машина · применимо и не поставлено · не применимо и почему.",
|
|
414
|
-
anyway: (cmd) => `Всё равно поставить сторожей дня 0: ${cmd}`,
|
|
415
|
-
expectRed: "Готовься к красному: сторож, поставленный на живой код, краснеет на нём весь.",
|
|
416
|
-
expectRedFix: (cmd) => `Это лечится храповиком — ${cmd}, — а не отключением.`,
|
|
417
|
-
installed: (n) => `Поставлено сторожей дня 0: ${n}`,
|
|
418
|
-
noDebt1: "Долга нет: на пустом проекте им нечего пропускать. Тот же сторож, поставленный",
|
|
419
|
-
noDebt2: "через полгода, покраснел бы на всём старом коде — и его бы выключили.",
|
|
420
|
-
allDeclared: "Все применимые записи уже объявлены.",
|
|
421
|
-
notYet: "Не применимо пока:",
|
|
422
|
-
notYetWhy: "Появится признак — запись покажется сама.",
|
|
423
|
-
orderTitle: "Порядок, в котором это делают:",
|
|
424
|
-
o1: "Задача словами.",
|
|
425
|
-
o1What: "Что и кому, без единого технического слова.",
|
|
426
|
-
o1Why: "Пока задача не описана словами, любая архитектура защищает неизвестно что.",
|
|
427
|
-
o2: "Ограничения.",
|
|
428
|
-
o2What: "Сроки, деньги, нагрузка, чем нельзя пользоваться.",
|
|
429
|
-
o2Why: "Ограничения выбирают решение куда чаще, чем вкус: без них выбирают вкусом.",
|
|
430
|
-
o3: "Сайзинг.",
|
|
431
|
-
o3What: "Сколько данных, запросов, людей — числами, хотя бы порядком.",
|
|
432
|
-
o3Why: "Число отделяет «нужна очередь» от «хватит таблицы». Без него спорят словами.",
|
|
433
|
-
o4: "Архитектура.",
|
|
434
|
-
o4What: "И только теперь — из первых трёх, а не до них.",
|
|
435
|
-
softNote1: "Этот порядок программа не проверяет: он в .aqk/docs/, и агент может его",
|
|
436
|
-
softNote2: "проигнорировать. Машина держит другое — сторожей выше. Разница между",
|
|
437
|
-
softNote3: "мягким и жёстким тут ровно такая: текст просят, команду выполняют.",
|
|
438
|
-
next: "Дальше:",
|
|
439
|
-
nextWhy: "— прогнать всё, что объявлено",
|
|
440
|
-
},
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
prove: {
|
|
444
|
-
title: "aqk prove — доказательство гейтов",
|
|
445
|
-
running: "доказываю гейты по их образцам…",
|
|
446
|
-
proven: (n) => `доказано: ${n}`,
|
|
447
|
-
broken: (n) => `не ловят: ${n}`,
|
|
448
|
-
unprovable: (n) => `нечем доказать: ${n}`,
|
|
449
|
-
okRed: "краснеет на красном, молчит на зелёном",
|
|
450
|
-
redPassed: "промолчал на КРАСНОМ образце — гейт не ловит брак",
|
|
451
|
-
greenFailed: "покраснел на ЗЕЛЁНОМ образце — гейт ругается на исправный код",
|
|
452
|
-
empty: "команда пустая — объявление без команды защиты не даёт",
|
|
453
|
-
noSamples: "нет образцов — доказать нечем",
|
|
454
|
-
noTarget: "не видно, куда подставить каталог образца — команда написана руками",
|
|
455
|
-
otherRecipe: (lang) => `образцы написаны под рецепт «${lang}», а стоит другой — доказать нечем`,
|
|
456
|
-
noGates: "гейтов не объявлено — доказывать нечего",
|
|
457
|
-
noSamplesDir: "в .aqk.yml не заполнено поле samples — образцы искать негде",
|
|
458
|
-
nothingProven:
|
|
459
|
-
"ни один гейт не доказан. Уровень выше AQK-1 означал бы доверие к автору, а не факт:\n проект с гейтом «true» прошёл бы его так же, как проект с настоящей защитой.",
|
|
460
|
-
fix: (cmd) => `почини: поставь запись каталога вместе с образцами — ${cmd}`,
|
|
461
|
-
heading: "Доказательство гейтов",
|
|
462
|
-
},
|
|
463
|
-
badge: {
|
|
464
|
-
noManifest: (cmd) => `Нет .aqk.yml — уровня ещё нет. Начни с ${cmd}`,
|
|
465
|
-
notReached: (cmd) => `AQK-0 не достигнут — значок выдавать не за что. Чего не хватает: ${cmd}`,
|
|
466
|
-
redGates: (n, names) =>
|
|
467
|
-
`Красных гейтов: ${n} (${names}). Значок при красном гейте — заявление автора, а не факт машины.`,
|
|
468
|
-
hint: (n) => `Проверено прогоном. Зелёных гейтов: ${n}. Строку выше — в README.`,
|
|
469
|
-
keepTrue: (cmd) => `Чтобы значок не превратился во враньё, поставь в конвейер: ${cmd}`,
|
|
470
|
-
checkMissing: (places) => `Значка AQK нет ни в одном из файлов: ${places}. Вставить нужно этот:`,
|
|
471
|
-
checkMismatch: (where, level) => `Значок врёт: ${where}, а прогон говорит AQK-${level}. Заменить на:`,
|
|
472
|
-
checkOk: (level, where) => `Значок совпал с прогоном: AQK-${level} — ${where}`,
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
224
|
+
...ruGates,
|
|
477
225
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// tool/lib/banner.mjs — заставка. Первая встреча человека с комплектом.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОНА ЕСТЬ. Жалоба владельца: «скачал и че дальше, не понятно, работает он вообще или
|
|
4
|
+
// нет». Строка присутствия в прогоне отвечает на «работает ли», а заставка — на «что это
|
|
5
|
+
// вообще такое»: она показывается там, где человек встречается с инструментом впервые.
|
|
6
|
+
//
|
|
7
|
+
// ПОКАЗЫВАЕТСЯ ДВАЖДЫ ЗА ВСЮ ЖИЗНЬ: при `init` и по `--version`. На каждой команде — нет:
|
|
8
|
+
// то, что видишь тридцатый раз, перестаёт читаться, а рядом стоят находки, которые читать надо.
|
|
9
|
+
//
|
|
10
|
+
// РИСУНОК — СВОЙ. Готовые работы из чужих подборок красивы, но у них нет ни автора, ни
|
|
11
|
+
// лицензии, а пакет уезжает в npm под MIT. Вставить чужое молча — сделать ровно то, за что мы
|
|
12
|
+
// ругаем других. Этот прислан владельцем; надпись собрана шрифтом ANSI Shadow, к шрифтам
|
|
13
|
+
// вопросов о правах нет.
|
|
14
|
+
// Отступ у кошки ОДИН пробел, у букв — два, и это не опечатка. Пустой Брайль `⠀` занимает
|
|
15
|
+
// место, но ничего не рисует: у рисунка видимая часть начинается со второго знака, и при
|
|
16
|
+
// одинаковом отступе кошка выглядела сдвинутой вправо на целый знак. Центры сведены по
|
|
17
|
+
// ВИДИМОЙ части, а не по началу строки.
|
|
18
|
+
const ART = [
|
|
19
|
+
" ⠀⠀⠀⠀⢀⣾⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢧⡀",
|
|
20
|
+
" ⠀⠀⠀⠀⠀⢀⣾⠃⢘⣷⣀⣀⣤⣤⣤⣄⣀⢸⡏⠈⢷⡀",
|
|
21
|
+
" ⠀⠀⠀⠀⠀⣼⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⣷",
|
|
22
|
+
" ⠀⠀⠀⠀⢰⣿⣿⣿⣿⠁⠸⠗⢹⣿⡟⠰⠷⠈⢻⣿⣿⣿⡇",
|
|
23
|
+
" ⠀⠀⢐⣒⣺⣿⣿⣿⣿⣀⠀⣀⣼⠛⢷⣄⠀⢀⣼⣿⣿⣿⣿⣉⡁",
|
|
24
|
+
" ⠀⠀⠀⠤⢺⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⠢",
|
|
25
|
+
"",
|
|
26
|
+
" █████╗ ██████╗ ██╗ ██╗",
|
|
27
|
+
" ██╔══██╗██╔═══██╗██║ ██╔╝",
|
|
28
|
+
" ███████║██║ ██║█████╔╝",
|
|
29
|
+
" ██╔══██║██║▄▄ ██║██╔═██╗",
|
|
30
|
+
" ██║ ██║╚██████╔╝██║ ██╗",
|
|
31
|
+
" ╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Объявленная ширина сверяется с настоящей модульной проверкой: правка рисунка, разъехавшаяся
|
|
35
|
+
// с этим числом, роняет её. Иначе число здесь через месяц врёт, как всякая цифра в комментарии.
|
|
36
|
+
const BANNER_WIDTH = 27;
|
|
37
|
+
|
|
38
|
+
// Терминал без UTF-8 превратит Брайль в вопросительные знаки, и человек решит, что инструмент
|
|
39
|
+
// сломан. Тогда честнее короткая строка: она несёт то же самое, только буквами.
|
|
40
|
+
function canDrawArt(env) {
|
|
41
|
+
if (String(env.AQK_NO_ART || "") === "1") return false;
|
|
42
|
+
const loc = `${env.LC_ALL || ""}${env.LC_CTYPE || ""}${env.LANG || ""}`.toLowerCase();
|
|
43
|
+
// Windows-терминалы локаль обычно не выставляют вовсе, а UTF-8 там с 2019 года есть.
|
|
44
|
+
// Поэтому пусто — рисуем; явно не-UTF-8 — не рисуем.
|
|
45
|
+
if (!loc) return true;
|
|
46
|
+
return loc.includes("utf-8") || loc.includes("utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ВЕРСИИ В ЗАСТАВКЕ НЕТ — решение владельца. Выпуски идут часто, и номер в картинке устаревает
|
|
50
|
+
// быстрее всего остального; за версией есть `--version`, где она и печатается отдельной строкой.
|
|
51
|
+
function banner(env = process.env) {
|
|
52
|
+
if (!canDrawArt(env)) return " aqk — agent quality kit";
|
|
53
|
+
// Отступ подписи — шесть, а не три: он посчитан от ВИДИМОГО центра букв (со 2-го знака по
|
|
54
|
+
// 26-й, центр 14), а не подобран на глаз. Подобранный на глаз держался до первой правки
|
|
55
|
+
// рисунка; посчитанный переживёт её, а проверка ниже покажет, если разъедется.
|
|
56
|
+
return `${ART.join("\n")}\n agent quality kit`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { banner, BANNER_WIDTH, canDrawArt };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CWD, PKG_ROOT, TARGET_DIR, SELF, c } from "./core.mjs";
|
|
4
|
+
import { L } from "../i18n/index.mjs";
|
|
5
|
+
import { canDrawArt } from "./banner.mjs";
|
|
6
|
+
// tool/lib/brief.mjs — короткая строка присутствия для прогона в хуке.
|
|
7
|
+
//
|
|
8
|
+
// ЗАЧЕМ ЭТО СУЩЕСТВУЕТ. Хук pre-commit молчит на успехе: вывод показывается только при провале
|
|
9
|
+
// — это его умолчание, проверено по документации 2026-09-08. То есть комплект, который всё
|
|
10
|
+
// держит, для человека НЕОТЛИЧИМ от невставленного: он поставил, поработал неделю и не увидел
|
|
11
|
+
// ни строки. «Скачал и че дальше» — дословная жалоба владельца.
|
|
12
|
+
//
|
|
13
|
+
// Это ровно тот порок, против которого написан весь комплект, только у нас самих: тишина
|
|
14
|
+
// неотличима от успеха. Поэтому строка печатается ВСЕГДА, в том числе — и особенно — когда
|
|
15
|
+
// всё хорошо. Одна строка: присутствие видно, читать нечего.
|
|
16
|
+
//
|
|
17
|
+
// СОВЕТ ОТДЕЛЬНО И РЕДКО. Вторая строка называет ОДНУ непоставленную запись и способ отказаться.
|
|
18
|
+
// Не список: список читается как «у вас всё плохо» и не помогает выбрать. Не каждый коммит:
|
|
19
|
+
// то, что видишь тридцатый раз, перестаёт читаться — и пролистывается вместе с настоящими
|
|
20
|
+
// находками, стоящими рядом.
|
|
21
|
+
|
|
22
|
+
// Сутки. Не «раз в прогон» и не «раз в неделю»: за сутки человек успевает забыть, но не успевает
|
|
23
|
+
// устать. Число здесь спорное — важно, что ограничитель есть и он машинный.
|
|
24
|
+
const ADVICE_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
// ЗНАЧОК ПРИСУТСТВИЯ — здесь, а не в каталогах строк. Символ один на оба языка, и держать его
|
|
27
|
+
// в двух местах значит однажды получить разные значки в ru и en: то же правило, по которому у
|
|
28
|
+
// нас один свод правил на две точки входа. Выбран владельцем из пятидесяти семи вариантов.
|
|
29
|
+
// В терминале без UTF-8 он превратится в мусор — там правило то же, что у заставки, и оно
|
|
30
|
+
// одно на двоих: разойдись эти два условия, и значок рисовался бы там, где картинка уже нет.
|
|
31
|
+
const MARK = "❖";
|
|
32
|
+
|
|
33
|
+
function briefLine(state, L, env = process.env) {
|
|
34
|
+
const t = L.brief;
|
|
35
|
+
const mark = canDrawArt(env) ? `${MARK} ` : "";
|
|
36
|
+
const parts = [t.held(state.held), t.todo(state.todo)];
|
|
37
|
+
if (state.level >= 0) parts.push(`AQK-${state.level}`);
|
|
38
|
+
else parts.push(t.levelUnknown);
|
|
39
|
+
const head = `${mark}${t.name} ${parts.join(" · ")}`;
|
|
40
|
+
if (!state.red || !state.red.length) return head;
|
|
41
|
+
return `${head}\n${t.red(state.red.join(", "))}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// «Не знаем, когда показывали» и «показывали давно» — одно и то же решение: показать.
|
|
45
|
+
// Испорченная отметка попадает сюда же намеренно: молчать из-за нечитаемого файла состояния
|
|
46
|
+
// значит потерять совет навсегда и не сказать почему.
|
|
47
|
+
function adviceDue(lastIso, now = Date.now()) {
|
|
48
|
+
if (!lastIso) return true;
|
|
49
|
+
const t = Date.parse(String(lastIso));
|
|
50
|
+
if (!Number.isFinite(t)) return true;
|
|
51
|
+
return now - t >= ADVICE_EVERY_MS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Первая из непоставленных, а не «самая важная»: важность мы не считаем, а порядок каталога
|
|
55
|
+
// осмыслен — записи в нём лежат от общего к частному. Выдавать порядок за приоритет нельзя.
|
|
56
|
+
function pickAdvice(todo = []) {
|
|
57
|
+
return todo.length ? todo[0] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// УВЕДОМЛЕНИЕ ОБ ОБНОВЛЕНИИ — и почему НЕ автообновление.
|
|
61
|
+
//
|
|
62
|
+
// Выпуски идут часто, а у половины способов установки версия закреплена и сама не двигается:
|
|
63
|
+
// `rev:` у pre-commit, тег у GitHub Action. Человек ставит комплект, получает версию с уже
|
|
64
|
+
// исправленной ошибкой и не узнаёт об этом никогда. У `npx` без версии проблемы нет — он берёт
|
|
65
|
+
// свежее при каждом запуске.
|
|
66
|
+
//
|
|
67
|
+
// АВТООБНОВЛЕНИЯ НЕТ, И ЭТО РЕШЕНИЕ, А НЕ НЕДОДЕЛКА. В тот же день выпущена запись каталога,
|
|
68
|
+
// краснеющая на `@latest`: «версия не закреплена — завтра приедет другая». Инструмент, который
|
|
69
|
+
// молча подменяет себя, стоя на воротах коммита, делал бы ровно то, что мы запрещаем другим.
|
|
70
|
+
// Первый же внешний читатель это заметит, и будет прав.
|
|
71
|
+
//
|
|
72
|
+
// СРАВНЕНИЕ ПО ЧИСЛАМ. Строкой «0.10.0» меньше «0.9.0», и уведомление пропало бы ровно на
|
|
73
|
+
// десятом выпуске — тихо и надолго. Такие поломки не замечают месяцами.
|
|
74
|
+
function cmpVer(a, b) {
|
|
75
|
+
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
|
76
|
+
const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
|
|
77
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
78
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
79
|
+
if (d) return d;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Совет зависит от способа установки: у pre-commit это `autoupdate`, а не npm. Совет мимо
|
|
85
|
+
// способа человек не выполнит — и перестанет читать следующие.
|
|
86
|
+
function updateNotice(current, latest, env = process.env, L) {
|
|
87
|
+
if (!current || !latest || cmpVer(latest, current) <= 0) return null;
|
|
88
|
+
const how = env.PRE_COMMIT ? L.brief.updateHookHow : L.brief.updateHow;
|
|
89
|
+
return `${L.brief.update(latest, current, how)}\n${L.brief.updateOff("AQK_UPDATE=0")}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// В конвейере не спрашиваем вовсе: там версия закреплена сознательно, читать уведомление
|
|
93
|
+
// некому, а лишний исходящий запрос из инструмента, который иначе не делает ни одного, —
|
|
94
|
+
// плохой размен. Отказ человека уважается тем же способом.
|
|
95
|
+
function updateWanted(env = process.env) {
|
|
96
|
+
if (String(env.AQK_UPDATE || "") === "0") return false;
|
|
97
|
+
if (env.CI || env.GITHUB_ACTIONS || env.GITLAB_CI) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// КРАТКИЙ РЕЖИМ для хука. Вывод целиком БУФЕРИЗУЕТСЯ, а печатается одна строка присутствия —
|
|
102
|
+
// и, при провале, весь буфер, чтобы человеку было что чинить. Перехват console.log выглядит
|
|
103
|
+
// грубо, и это осознанный размен: альтернатива — протащить флаг через четыреста строк печати,
|
|
104
|
+
// где каждая строка стала бы условной. Перехват локален, снимается в том же вызове и объяснён
|
|
105
|
+
// здесь; условие в каждой строке объяснить было бы негде.
|
|
106
|
+
function beginBrief() {
|
|
107
|
+
const lines = [];
|
|
108
|
+
const real = console.log;
|
|
109
|
+
console.log = (...a) => lines.push(a.join(" "));
|
|
110
|
+
return { lines, restore: () => { console.log = real; } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Печать краткого итога. Совет — не чаще раза в сутки и с явным способом отказаться: то, что
|
|
114
|
+
// видишь тридцатый раз, перестаёт читаться и пролистывается вместе с настоящими находками рядом.
|
|
115
|
+
// Отметка времени лежит в .aqk/, который в .gitignore: это состояние машины, а не проекта.
|
|
116
|
+
async function finishBrief(buf, state, todoRecs, ok) {
|
|
117
|
+
if (!buf) return;
|
|
118
|
+
buf.restore();
|
|
119
|
+
console.log(briefLine(state, L));
|
|
120
|
+
|
|
121
|
+
// Уведомление об обновлении — ДО разбора вердикта: оно от него не зависит. Сначала было
|
|
122
|
+
// после, и у любого проекта, где чего-то не хватает, версия не спрашивалась никогда —
|
|
123
|
+
// то есть у всех, кому комплект и нужен. Поймано первым же живым запуском.
|
|
124
|
+
await maybeUpdateNotice();
|
|
125
|
+
|
|
126
|
+
// При провале печатаем ВЕСЬ буфер: человеку нужно чинить, а одной строкой не починишь.
|
|
127
|
+
if (!ok) { console.log(buf.lines.join("\n")); return; }
|
|
128
|
+
|
|
129
|
+
if (process.env.AQK_ADVICE === "0" || !state.todo) return;
|
|
130
|
+
const stampFile = join(CWD, TARGET_DIR, "advice-shown");
|
|
131
|
+
let last = null;
|
|
132
|
+
try { last = (await readFile(stampFile, "utf8")).trim(); } catch { /* не показывали ещё */ }
|
|
133
|
+
if (!adviceDue(last)) return;
|
|
134
|
+
const advice = pickAdvice(todoRecs);
|
|
135
|
+
if (!advice) return;
|
|
136
|
+
console.log(c.dim(L.brief.advise(advice.slug, advice.intent || "")));
|
|
137
|
+
console.log(c.dim(L.brief.adviseOff(`${SELF} why ${advice.slug}`, "AQK_ADVICE=0")));
|
|
138
|
+
try {
|
|
139
|
+
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
140
|
+
await writeFile(stampFile, new Date().toISOString(), "utf8");
|
|
141
|
+
} catch { /* не смогли записать отметку — совет повторится, это не беда */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Спрашивает реестр npm о своей версии. РАЗ В СУТКИ, НЕ В КОНВЕЙЕРЕ, С ТАЙМАУТОМ, И МОЛЧА
|
|
145
|
+
// ПРИ ЛЮБОЙ ОШИБКЕ. До этой строки комплект не делал ни одного исходящего запроса — так
|
|
146
|
+
// написано в README и SECURITY.md, и там же теперь написано про этот. Сделать тихо то, за что
|
|
147
|
+
// мы ругаем других, нельзя: весь смысл в том, что заявленное совпадает с происходящим.
|
|
148
|
+
//
|
|
149
|
+
// Код возврата не меняется никогда: уведомление, роняющее коммит, выключат в тот же день —
|
|
150
|
+
// и вместе с ним всё остальное, что печатает эта строка.
|
|
151
|
+
async function maybeUpdateNotice() {
|
|
152
|
+
if (!updateWanted()) return;
|
|
153
|
+
const stamp = join(CWD, TARGET_DIR, "update-checked");
|
|
154
|
+
let last = null;
|
|
155
|
+
try { last = (await readFile(stamp, "utf8")).trim(); } catch { /* ещё не спрашивали */ }
|
|
156
|
+
if (!adviceDue(last)) return;
|
|
157
|
+
|
|
158
|
+
let current = "";
|
|
159
|
+
try { current = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || ""; } catch { return; }
|
|
160
|
+
|
|
161
|
+
// ОТМЕТКА СТАВИТСЯ ДО ЗАПРОСА, а не после удачного ответа. Сперва было наоборот, и замер
|
|
162
|
+
// показал цену: человек без сети платил бы ожиданием на КАЖДОМ коммите, а не раз в сутки.
|
|
163
|
+
// Из двух ошибок выбрана дешёвая: пропущенное за день уведомление против ежедневного стопора.
|
|
164
|
+
try {
|
|
165
|
+
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
166
|
+
await writeFile(stamp, new Date().toISOString(), "utf8");
|
|
167
|
+
} catch { /* не смогли записать — спросим ещё раз, это не беда */ }
|
|
168
|
+
|
|
169
|
+
let latest = "";
|
|
170
|
+
try {
|
|
171
|
+
// Три секунды, а не полторы. Замерено 2026-09-08: тёплый запрос к реестру — 533 мс,
|
|
172
|
+
// а первый, с разрешением имени и рукопожатием, в полторы секунды не уложился. Слишком
|
|
173
|
+
// тугой срок означал бы, что уведомление не приходит никогда и никто не знает почему.
|
|
174
|
+
const r = await fetch("https://registry.npmjs.org/agent-quality-kit/latest", {
|
|
175
|
+
signal: AbortSignal.timeout(3000),
|
|
176
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
177
|
+
});
|
|
178
|
+
if (!r.ok) return;
|
|
179
|
+
latest = String((await r.json()).version || "");
|
|
180
|
+
} catch {
|
|
181
|
+
// Сети нет, реестр молчит, таймаут — всё это НЕ повод сказать хоть слово. Инструмент,
|
|
182
|
+
// который жалуется на отсутствие интернета посреди коммита, выключают.
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const notice = updateNotice(current, latest, process.env, L);
|
|
187
|
+
if (notice) console.log(c.dim(notice));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Наружу — только то, что зовут снаружи. `cmpVer` и `ADVICE_EVERY_MS` внутренние: экспорт,
|
|
191
|
+
// который никто не импортирует, читается как часть договора и мешает менять внутренности.
|
|
192
|
+
export { briefLine, adviceDue, pickAdvice, updateNotice, updateWanted, beginBrief, finishBrief };
|