agent-quality-kit 0.9.0 → 0.10.1

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.
Files changed (68) hide show
  1. package/README.md +69 -10
  2. package/README.ru.md +68 -11
  3. package/kit/docs/ai/index.md +1 -0
  4. package/kit/docs/api-e2e.md +214 -0
  5. package/kit/docs/ready-made-rules.md +85 -0
  6. package/kit/gates/README.md +22 -0
  7. package/kit/gates/api-contract-has-arbiter/README.md +63 -0
  8. package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
  9. package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
  10. package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
  11. package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
  12. package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
  13. package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
  14. package/kit/gates/ci-actually-fails/check.sh +9 -1
  15. package/kit/gates/commit-explains-itself/check.sh +15 -0
  16. package/kit/gates/complexity-limit/red/deep.go +17 -0
  17. package/kit/gates/complexity-limit/red/deep.rs +17 -0
  18. package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
  19. package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
  20. package/kit/gates/protection-not-removed/README.md +67 -0
  21. package/kit/gates/protection-not-removed/check.sh +101 -0
  22. package/kit/gates/protection-not-removed/gate.yml +10 -0
  23. package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
  24. package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
  25. package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
  26. package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
  27. package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
  28. package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
  29. package/kit/gates/todo-without-task/red/later.go +6 -0
  30. package/kit/gates/todo-without-task/red/later.rs +4 -0
  31. package/llms.txt +10 -4
  32. package/package.json +2 -1
  33. package/tool/commands/context.mjs +36 -1
  34. package/tool/commands/doctor.mjs +56 -2
  35. package/tool/commands/gates.mjs +2 -0
  36. package/tool/commands/probe.mjs +228 -0
  37. package/tool/commands/vitals.mjs +11 -3
  38. package/tool/i18n/en-docs.mjs +10 -0
  39. package/tool/i18n/en-gates.mjs +309 -0
  40. package/tool/i18n/en.mjs +10 -281
  41. package/tool/i18n/ru-docs.mjs +10 -0
  42. package/tool/i18n/ru-gates.mjs +311 -0
  43. package/tool/i18n/ru.mjs +10 -280
  44. package/tool/lib/cadence.mjs +57 -0
  45. package/tool/lib/core.mjs +1 -0
  46. package/tool/lib/evidence.mjs +15 -2
  47. package/tool/lib/execution.mjs +67 -0
  48. package/tool/lib/history.mjs +82 -0
  49. package/tool/lib/manifest.mjs +1 -1
  50. package/tool/lib/protection.mjs +52 -0
  51. package/tool/lib/prove.mjs +40 -13
  52. package/tool/lib/repo.mjs +12 -2
  53. package/tool/program.mjs +7 -0
  54. package/tool/selfcheck/smoke/_fixture.mjs +89 -0
  55. package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
  56. package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
  57. package/tool/selfcheck/smoke/protection.test.mjs +101 -0
  58. package/tool/selfcheck/smoke/release-tools.test.mjs +55 -0
  59. package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
  60. package/tool/selfcheck/smoke.sh +194 -7
  61. package/tool/selfcheck/units-cadence.mjs +69 -0
  62. package/tool/selfcheck/units-context.mjs +24 -0
  63. package/tool/selfcheck/units-evidence.mjs +34 -0
  64. package/tool/selfcheck/units-execution.mjs +88 -0
  65. package/tool/selfcheck/units-level.mjs +32 -1
  66. package/tool/selfcheck/units-probe.mjs +100 -0
  67. package/tool/selfcheck/units-repo.mjs +31 -1
  68. package/tool/selfcheck/units-vitals.mjs +19 -0
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: "ещё и запустить объявленные гейты",
@@ -127,6 +130,11 @@ export const ru = {
127
130
  manifestUnparsedWhy:
128
131
  "имена полей и гейтов пишутся латиницей, цифрами, дефисом и подчёркиванием. Объявленное\n здесь не выполняется вовсе — а выглядит так, будто выполняется.",
129
132
  manifestKnown: (keys) => `Поля манифеста: ${keys.join(", ")}`,
133
+ runVerdictOk: "Всё объявленное зелёное.",
134
+ runVerdictFail: (why) => `Прогон красный: ${why}.`,
135
+ whyMissing: "не хватает файлов из шапки",
136
+ whyLevel: "ступень не взята",
137
+ whyGates: (n, names) => `красных гейтов ${n}${names ? ` (${names})` : ""}`,
130
138
  thresholdPass: (min) => `Порог AQK-${min} пройден.`,
131
139
  thresholdFail: (min, now) => `Порог AQK-${min} НЕ пройден: сейчас AQK-${now}.`,
132
140
  // Ступень взята, но прогон красный — это другое утверждение, и виновника называем сразу:
@@ -186,6 +194,7 @@ export const ru = {
186
194
  has_agent_entry: ["свода для агента здесь нет", "свод для агента есть"],
187
195
  has_ui: ["не видно стилей и компонентов интерфейса", "интерфейс есть: стили или компоненты"],
188
196
  has_mcp: ["агенту здесь не подключали внешних инструментов через MCP", "MCP-серверы объявлены"],
197
+ has_api_spec: ["не видно спецификации API", "спецификация API есть"],
189
198
  },
190
199
  },
191
200
 
@@ -212,284 +221,5 @@ export const ru = {
212
221
  alreadyDeclared: "уже объявлен",
213
222
  },
214
223
 
215
- add: {
216
- noSuchGate: (slug, cmd) => `Нет такого гейта: ${slug}\nСписок применимых — ${cmd}`,
217
- toolMissing: (slug, missing) =>
218
- `Записи ${slug} нужен ${missing.join(" и ")}, а его нет на этой машине.\n почини: поставь ${missing.join(" и ")} и повтори — запись делегирует готовому инструменту по устройству, своей проверки у неё нет.`,
219
- noRecipe: (slug, stack, missing) =>
220
- `У записи ${slug} нет команды ни под ${stack}, ни общей.` +
221
- (Array.isArray(missing) && missing.length
222
- ? `\n почини: поставь ${missing.join(" или ")} и повтори — запись делегирует готовому инструменту по устройству, своей проверки у неё нет.`
223
- : `\n почини: добавь рецепт под свой стек в gate.yml этой записи либо возьми другую.`),
224
- thisStack: "этот стек",
225
- needName: (cmd, doctor) => `Укажи имя гейта: ${cmd}. Список — ${doctor}`,
226
- noManifest: (cmd) => `Нет .aqk.yml — сначала ${cmd}`,
227
- notApplicable: (why) => `Этот гейт к репозиторию не применим: ${why}`,
228
- installAnyway: "Ставлю всё равно — решение твоё, но сторожить ему нечего.",
229
- copied: (n) => `${n} файлов: проверка и образцы`,
230
- declared: (cmd) => `гейт объявлен: ${cmd}`,
231
- notDeclared: (why, slug, cmd) => `не тронут (${why}). Впиши сам: ${slug}: "${cmd}"`,
232
- nextTitle: "Дальше:",
233
- next1: "Проверь, что он краснеет и молчит там, где должен:",
234
- expectFail: "→ ожидается отказ",
235
- expectSilence: "→ ожидается тишина",
236
- next2: "Впиши команду в хук коммита и в конвейер.",
237
- next2Why: "Гейт, который никто не запускает, — не гейт.",
238
- next3: (cmd) => `Прогон всех объявленных: ${cmd}`,
239
- },
240
-
241
- gnew: {
242
- needName: (cmd) => `Укажи имя: ${cmd}`,
243
- badName: (slug) => `Имя «${slug}» не годится: латиница через дефис, например secrets-not-in-code.\nИмя читают в чужих проектах — оно часть словаря.`,
244
- looksExisting: (slug) => `Похоже, такое уже есть: ${slug}`,
245
- recipeNotGate: "Рецепт под другой стек — это строка в recipes существующей записи.",
246
- forceHint: (cmd) => `Всё равно завести новую: ${cmd}`,
247
- exists: (path) => `${path} уже существует.`,
248
- nextTitle: "Дальше — по порядку:",
249
- n1: "Проверь, нет ли готового правила",
250
- n1Where: "в ruff, eslint, semgrep.",
251
- n1Why: "Готовое точнее, подробнее и его поддерживают без тебя. Своя проверка — запасная.",
252
- n2: "Положи образцы.",
253
- n2Red: "— код, на котором проверка обязана сработать.",
254
- n2Green: "— тот же код, но правильный.",
255
- n2Why: "Зелёный важнее: он ловит проверку, которая краснеет на исправном коде.",
256
- n3: "Напиши проверку",
257
- n3Where: "в check.sh. В тексте отказа — что именно сделать.",
258
- n4: "Заполни gate.yml:",
259
- n4What: "намерение, триггер, доказательство отказом.",
260
- n5: "Прогони:",
261
- n5Why: "Арбитр обязан покраснеть на red/ и промолчать на green/. Не прошло — не запись.",
262
- },
263
-
264
- ratchet: {
265
- needName: (cmd) => `Укажи гейт: ${cmd}. Он должен быть уже объявлен в .aqk.yml`,
266
- noManifest: (cmd) => `Нет .aqk.yml — сначала ${cmd}`,
267
- notDeclared: (slug, cmd) => `Гейт «${slug}» не объявлен в .aqk.yml. Сначала: ${cmd}`,
268
- already: (slug) => `На гейте «${slug}» храповик уже стоит.`,
269
- notRunnable: (slug, cmd) =>
270
- `Гейт «${slug}» не запускается: ${cmd}\n` +
271
- `Снимать долг с несуществующего сторожа нельзя — в реестр попадут его же сообщения\n` +
272
- `об ошибке, и он станет разрешением. Сначала почини команду.`,
273
- registryHead: (slug, stamp) =>
274
- `# Реестр долга: ${slug}\n` +
275
- `# Снят ${stamp}. Список разрешается ТОЛЬКО укорачивать.\n` +
276
- `# Новое нарушение красит гейт; исправленное вычёркивается автоматически.\n` +
277
- `#\n` +
278
- `# Долг без цели и срока не кончается. Цель — сколько нарушений считать погашенным долгом;\n` +
279
- `# по достижении храповик скажет убрать обёртку. Срок необязателен, но если он есть,\n` +
280
- `# после этой даты непогашенный долг красит гейт — срок без последствия не срок.\n` +
281
- `# aqk-goal: 0\n` +
282
- `# aqk-deadline:\n`,
283
- recorded: (n) => `${n} нарушений записано долгом`,
284
- libCopied: "обёртка скопирована в проект",
285
- wrapped: "команда завёрнута в храповик",
286
- changesTitle: "Что это меняет:",
287
- fromToday: "со дня установки",
288
- changes1: (fromToday) => `Правило действует ${fromToday}. Старый код трогать не надо, но новое`,
289
- changes2: "нарушение того же класса гейт не пропустит.",
290
- test1: "Проверка, что это храповик, а не советчик: «может ли новый код добавить нарушение",
291
- test2: "и пройти?» Может — значит гейта нет.",
292
- run: (cmd) => `Прогнать: ${cmd}`,
293
- },
294
-
295
- find: {
296
- needQuery: (cmd) => `Опиши намерение словами: ${cmd}`,
297
- example: 'find "отладочная печать не доезжает до прода"',
298
- exists: "Такое уже есть — новую запись заводить не надо:",
299
- match: (pct) => `совпадение ${pct}%`,
300
- recipes: (langs) => `рецепты: ${langs}общий`,
301
- existsWhy1: "Если у тебя рецепт под другой стек — это строка в recipes существующей",
302
- existsWhy2: "записи, а не новый гейт. Намерение одно, исполнителей может быть много.",
303
- near: "Точного совпадения нет, но рядом лежит:",
304
- nearWhy: "Прочитай их README. Если намерение то же — дополняй, а не заводи новое.",
305
- none: "Такого намерения в каталоге нет.",
306
- journal: "В журнале есть шишка на эту тему:",
307
- journalWhy: "Шишка записана — значит доказательство для новой записи уже есть.",
308
- howTitle: "Как добавить свой гейт:",
309
- how1: "Назови отказ.",
310
- how1What: "Какой конкретный брак он поймал в живом проекте, чего это стоило.",
311
- how1Why: "«Это хорошая практика» не принимается: так каталог набирает сотни пунктов и умирает.",
312
- how2: "Заведи папку",
313
- how2What: "— gate.yml, red/, green/, README.md.",
314
- how2Why: "Норма записи со всеми полями — kit/gates/README.md",
315
- how3: "Проверь машиной:",
316
- how3Why: "Арбитр обязан покраснеть на red/ и промолчать на green/. Не прошло — не запись.",
317
- how4: "Пришли изменением",
318
- how4What: "в репозиторий комплекта.",
319
- },
320
-
321
- why: {
322
- needQuery: (cmd) => `Опиши, что пропустили: ${cmd}`,
323
- example: 'why "файл вырос до девяти тысяч строк"',
324
- ciAtOnce: "разом: doctor --run",
325
- ciOwnStep: "отдельным шагом",
326
- unsure: "Уверенного совпадения нет. Похоже на эти записи:",
327
- unsureByName: (cmd) => `Назови запись именем: ${cmd}`,
328
- unsureNone: (cmd) => `Ни одна не подходит — значит сторожа не было: ${cmd}`,
329
- decideTitle: "Дальше решаешь ты:",
330
- decideQ: "это твоя частность или общий случай?",
331
- decideWhy: "Общий — идёт в каталог и достаётся всем. Частный — остаётся у тебя.",
332
- decideNote: (cmd) => `Урок в общий журнал в любом случае: ${cmd}`,
333
- fix: "Почини так:",
334
- noGuard: "Сторожа не было.",
335
- noGuardWhy: "В каталоге нет записи с таким намерением.",
336
- noGuardFix: (cmd) => `заведи запись — ${cmd}`,
337
- noGuardHint: "Красный образец бери прямо из этой поломки: она уже случилась, выдумывать нечего.",
338
- closest: (slug, pct) => `Ближайшая запись каталога: ${slug} совпадение ${pct}%`,
339
- notInstalled: "Сторож есть в каталоге, но в этом проекте не поставлен.",
340
- notInstalledHint1: "Он покраснеет на старом коде — это нормально: старое закрывается храповиком,",
341
- notInstalledHint2: (cmd) => `новое ловится со дня установки. ${cmd}`,
342
- declaredAs: (cmd) => `Объявлен: ${cmd}`,
343
- notRunning: "Сторож объявлен, но не запускается.",
344
- notRunningWhy: "Худший случай: тишина читается как успех.",
345
- notRunningFix: "путь или программа из команды не существуют — проверь их.",
346
- notRunningHint: "Отсутствие сигнала неотличимо от успеха, поэтому это не «мелочь в конфиге».",
347
- bypassed: "Сторож есть и эту поломку ловит — значит его обошли.",
348
- noCiFix: "конвейера нет. Проверка, которую гоняет только человек,",
349
- noCiHint: "работает ровно до первого «забыл».",
350
- notInCiFix: "конвейер есть, но этот гейт в нём не запускается.",
351
- notInCiHint: (cmd) => `Дешевле всего одним шагом: ${cmd} — он гоняет всё объявленное.`,
352
- inCiFix: (how) => `конвейер его гоняет (${how}) — значит красный прогон`,
353
- inCiHint1: "кто-то пропустил или обошёл. Перенеси правило из текста в механику:",
354
- inCiHint2: "блокирующий шаг, а не необязательный; запрет слияния при красном.",
355
- blind: "Сторож есть, стоит и запускается — но этой поломки не видит.",
356
- blindFix: (dir) => `положи в ${dir} кусок кода из этой поломки`,
357
- blindHint1: "и доведи проверку до красного на нём. Порядок обратный привычному: сначала",
358
- blindHint2: "образец, потом правка — иначе непонятно, что именно починено.",
359
- blindCheck: "Проверить после правки: bash tool/selfcheck/gates.sh",
360
- },
361
-
362
- init: {
363
- noDocs: (dir) => `Не найден корпус методичек: ${dir}\nПохоже, пакет установлен не полностью.`,
364
- docsRu:
365
- "методички в .aqk/docs/ остаются на русском — решение, а не недоделка.",
366
- created: (n) => `создано (${n}):`,
367
- andMore: (n) => `… и ещё ${n}`,
368
- kept: (n) => `уже были на месте, не тронуты (${n}):`,
369
- overwrite: (cmd) => `перезаписать: ${cmd}`,
370
- nextTitle: "Что дальше — по порядку:",
371
- n1a: "Открой",
372
- n1b: "и заполни раздел «Команды». Команда, которую нельзя",
373
- n1c: "скопировать и выполнить, — не команда, а пожелание.",
374
- n2a: "Прочитай",
375
- n2b: "— это обязательный минимум",
376
- n2c: "проекта без привязки к языку. Пройди сверху вниз и отметь, чего нет.",
377
- n3a: "Заполни",
378
- n3b: "— гейты, образцы, журнал. Уровень соответствия AQK",
379
- n3c: (cmd) => `считается по нему: ${cmd}.`,
380
- n4a: "Поднимайся по ступеням",
381
- n4b: "по одной",
382
- n4c: ". Гейт стережёт существующий артефакт:",
383
- n4d: "проверка на код, которого ещё нет, — мёртвое правило.",
384
- burned: (cmd) => `Обжёгся на чём-то — запиши: ${cmd}`,
385
- hookHint: (cmd) => `Claude Code: ${cmd} — состояние проекта будет попадать в контекст агента\n само, до первого действия. Для других агентов хука нет, и это не недоделка.`,
386
- },
387
-
388
- feedback: {
389
- title: "Если пригодилось:",
390
- star: (url) => `Поставь звезду — ${url}`,
391
- issue: "Нашёл баг или не подошло — заведи Issue, самая полезная обратная связь: и то и другое.",
392
- notRemembered: "запомнить не удалось (дом недоступен для записи) — сообщение появится снова",
393
- once: "Это разовое сообщение: больше не покажется на этой машине.",
394
- },
395
-
396
- note: {
397
- journalTitle: "Журнал шишек",
398
- needTitle: (cmd) => `Нужен заголовок: ${cmd}`,
399
- noJournal: (url) => `Клон журнала не найден.\nСделай один раз:\n git clone ${url}.git ~/projects/aqk\nили укажи путь: export AQK_HOME=/путь/к/aqk`,
400
- journalMissing: (path) => `Журнал не найден: ${path}`,
401
- emptyBody: `Тело записи пустое. Передай его на стандартный ввод, например:\n\n aqk note "заголовок" <<'EOF'\n **Что случилось.** ...\n **Чем это стоило.** ...\n **Вывод.** 🔧 ...\n EOF`,
402
- noOutcome:
403
- "В записи нет отметки решения. Урок без вывода — это история, а не урок.\n" +
404
- "Припиши одну из трёх:\n" +
405
- " ✅ **Стало гейтом:** <имя записи каталога>\n" +
406
- " 🔧 **Стало правкой оснастки:** <что именно изменено>\n" +
407
- " 👤 **Гейтом не станет:** <почему>",
408
- unknownProject: "неизвестно",
409
- projectField: "Проект",
410
- pushed: (title) => `Записано и отправлено: ${title}`,
411
- localOnly: (cmd) => `Записано локально, push не прошёл. Отправить: ${cmd}`,
412
- },
413
-
414
- blob: {
415
- noDocs: (dir) => `Не найдены методички: ${dir}`,
416
- header: (stamp) =>
417
- `<!-- СОБРАНО КОМАНДОЙ aqk blob ${stamp} из kit/docs. Не править руками:\n` +
418
- ` правки затрёт следующая сборка. Источник — отдельные файлы. -->\n\n` +
419
- `# AQK — методички одним файлом\n`,
420
- source: (path) => `источник: ${path}`,
421
- done: (n, kb) => `GOD_AI.md — ${n} файлов, ${kb} КБ`,
422
- rebuilt: "Собирается заново каждой командой. Править надо оригиналы в kit/docs.",
423
- },
424
-
425
- start: {
426
- noRecipeHere: "нужен инструмент, которого нет на этой машине",
427
- initFailed: (cmd) => `Не получилось разложить комплект. Начни с ${cmd}`,
428
- tooManyFiles: (n) => `В репозитории уже ${n} файлов кода — это другой сценарий.`,
429
- useDoctor: (cmd) => `${cmd} осмотрит, что есть, и разделит записи на три списка:`,
430
- threeLists: "держит машина · применимо и не поставлено · не применимо и почему.",
431
- anyway: (cmd) => `Всё равно поставить сторожей дня 0: ${cmd}`,
432
- expectRed: "Готовься к красному: сторож, поставленный на живой код, краснеет на нём весь.",
433
- expectRedFix: (cmd) => `Это лечится храповиком — ${cmd}, — а не отключением.`,
434
- installed: (n) => `Поставлено сторожей дня 0: ${n}`,
435
- noDebt1: "Долга нет: на пустом проекте им нечего пропускать. Тот же сторож, поставленный",
436
- noDebt2: "через полгода, покраснел бы на всём старом коде — и его бы выключили.",
437
- allDeclared: "Все применимые записи уже объявлены.",
438
- notYet: "Не применимо пока:",
439
- notYetWhy: "Появится признак — запись покажется сама.",
440
- orderTitle: "Порядок, в котором это делают:",
441
- o1: "Задача словами.",
442
- o1What: "Что и кому, без единого технического слова.",
443
- o1Why: "Пока задача не описана словами, любая архитектура защищает неизвестно что.",
444
- o2: "Ограничения.",
445
- o2What: "Сроки, деньги, нагрузка, чем нельзя пользоваться.",
446
- o2Why: "Ограничения выбирают решение куда чаще, чем вкус: без них выбирают вкусом.",
447
- o3: "Сайзинг.",
448
- o3What: "Сколько данных, запросов, людей — числами, хотя бы порядком.",
449
- o3Why: "Число отделяет «нужна очередь» от «хватит таблицы». Без него спорят словами.",
450
- o4: "Архитектура.",
451
- o4What: "И только теперь — из первых трёх, а не до них.",
452
- softNote1: "Этот порядок программа не проверяет: он в .aqk/docs/, и агент может его",
453
- softNote2: "проигнорировать. Машина держит другое — сторожей выше. Разница между",
454
- softNote3: "мягким и жёстким тут ровно такая: текст просят, команду выполняют.",
455
- next: "Дальше:",
456
- nextWhy: "— прогнать всё, что объявлено",
457
- },
458
-
459
-
460
- prove: {
461
- title: "aqk prove — доказательство гейтов",
462
- running: "доказываю гейты по их образцам…",
463
- proven: (n) => `доказано: ${n}`,
464
- broken: (n) => `не ловят: ${n}`,
465
- unprovable: (n) => `нечем доказать: ${n}`,
466
- okRed: "краснеет на красном, молчит на зелёном",
467
- redPassed: "промолчал на КРАСНОМ образце — гейт не ловит брак",
468
- greenFailed: "покраснел на ЗЕЛЁНОМ образце — гейт ругается на исправный код",
469
- empty: "команда пустая — объявление без команды защиты не даёт",
470
- noSamples: "нет образцов — доказать нечем",
471
- noTarget: "не видно, куда подставить каталог образца — команда написана руками",
472
- otherRecipe: (lang) => `образцы написаны под рецепт «${lang}», а стоит другой — доказать нечем`,
473
- noGates: "гейтов не объявлено — доказывать нечего",
474
- needsProgram: (progs) => `НЕ ПРОВЕРЕНА здесь — нужен «${progs}»`,
475
- noSamplesDir: "в .aqk.yml не заполнено поле samples — образцы искать негде",
476
- nothingProven:
477
- "ни один гейт не доказан. Уровень выше AQK-1 означал бы доверие к автору, а не факт:\n проект с гейтом «true» прошёл бы его так же, как проект с настоящей защитой.",
478
- fix: (cmd) => `почини: поставь запись каталога вместе с образцами — ${cmd}`,
479
- heading: "Доказательство гейтов",
480
- },
481
- badge: {
482
- noManifest: (cmd) => `Нет .aqk.yml — уровня ещё нет. Начни с ${cmd}`,
483
- notReached: (cmd) => `AQK-0 не достигнут — значок выдавать не за что. Чего не хватает: ${cmd}`,
484
- redGates: (n, names) =>
485
- `Красных гейтов: ${n} (${names}). Значок при красном гейте — заявление автора, а не факт машины.`,
486
- hint: (n) => `Проверено прогоном. Зелёных гейтов: ${n}. Строку выше — в README.`,
487
- keepTrue: (cmd) => `Чтобы значок не превратился во враньё, поставь в конвейер: ${cmd}`,
488
- checkMissing: (places) => `Значка AQK нет ни в одном из файлов: ${places}. Вставить нужно этот:`,
489
- checkMismatch: (where, level) => `Значок врёт: ${where}, а прогон говорит AQK-${level}. Заменить на:`,
490
- checkOk: (level, where) => `Значок совпал с прогоном: AQK-${level} — ${where}`,
491
- },
492
-
493
-
494
-
224
+ ...ruGates,
495
225
  };
@@ -0,0 +1,57 @@
1
+ // tool/lib/cadence.mjs — когда комплект делает работу САМ, не дожидаясь, что о ней вспомнят.
2
+ //
3
+ // ЗАЧЕМ. Владелец сформулировал точнее, чем было в замысле: «команду, о которой надо вспомнить,
4
+ // агент не вспомнит, а человек о ней не узнает». Это тот же класс, что файл, который можно не
5
+ // прочитать, — и весь комплект написан против него. Команда `probe`, которую надо запускать
6
+ // руками, наполовину бесполезна по построению: она отвечает на важнейший вопрос («что здесь не
7
+ // прикрыто ничем») и не задаётся никем.
8
+ //
9
+ // РЕШЕНИЕ ТО ЖЕ, ЧТО У СОВЕТА И ПРОВЕРКИ ВЕРСИИ: не напоминать, а делать. Разница в единице.
10
+ // Совет считает сутки — он про внимание человека. Проба считает КОММИТЫ: репозиторий, в котором
11
+ // месяц не работали, перепроверять незачем, а сто коммитов за день перепроверить надо. Время
12
+ // здесь не при чём, при чём — сколько кода написано с прошлого раза.
13
+ //
14
+ // ПОЧЕМУ СТО. Число выбрано так, чтобы проба не мешала: на нашем репозитории это примерно
15
+ // две недели работы, а сама проба идёт секунды. Порог виден в выводе и меняется числом, а не
16
+ // прячется: правило, которого не видно, через месяц читается как случайность.
17
+ const PROBE_EVERY = 100;
18
+
19
+ // `last` — то, что записала прошлая проба: { at: <число коммитов на тот момент> }.
20
+ // `now` — сколько коммитов в репозитории сейчас; null, если счётчик взять не удалось.
21
+ //
22
+ // Пробы не было ВОВСЕ — она нужна, и нужна сразу, а не на сто первом коммите: молодой проект
23
+ // узнал бы о своих дырах позже всех, хотя закрывать их дешевле всего именно в начале.
24
+ function probeDue(last, now, every = PROBE_EVERY) {
25
+ if (now === null || now === undefined) return false;
26
+ const at = Number(last?.at);
27
+ if (!Number.isFinite(at)) return true;
28
+ return now - at >= every;
29
+ }
30
+
31
+ // Состояние для человека и для агента. ЧЕТЫРЕ исхода, и сливать их нельзя — по той же причине,
32
+ // по которой их четыре у `vitals`: «не делалась» и «свежая» различаются тем, что в первом
33
+ // случае мы ничего не знаем, а молчание читается как «всё хорошо».
34
+ function probeState(last, now, every = PROBE_EVERY) {
35
+ if (now === null || now === undefined) return { state: "unknown", behind: null };
36
+ const at = Number(last?.at);
37
+ if (!Number.isFinite(at)) return { state: "never", behind: null };
38
+ const behind = now - at;
39
+ return { state: behind >= every ? "stale" : "fresh", behind };
40
+ }
41
+
42
+ // Порог из манифеста: `probe: 250` — раз в двести пятьдесят коммитов, `probe: 0` — не делать
43
+ // вовсе. Число выбирается проектом, а не нами: сто коммитов на репозитории с десятком коммитов
44
+ // в час — это трижды в день, а на редком проекте они не наберутся никогда.
45
+ //
46
+ // Неразобранное значение НЕ молчит: строка `probe: часто` означала бы «человек настроил», а
47
+ // работал бы умолчательный порог — расхождение между написанным и происходящим, то самое,
48
+ // против чего весь комплект. Возвращается null, и вызывающий говорит об этом вслух.
49
+ function probeEvery(man) {
50
+ const raw = man?.probe;
51
+ if (raw === undefined || raw === null || String(raw).trim() === "") return PROBE_EVERY;
52
+ const n = Number(String(raw).trim());
53
+ if (!Number.isFinite(n) || n < 0) return null;
54
+ return n;
55
+ }
56
+
57
+ export { probeDue, probeState, probeEvery, PROBE_EVERY };
package/tool/lib/core.mjs CHANGED
@@ -71,6 +71,7 @@ function commandRows(L) {
71
71
  { name: "doctor", args: "--run", text: h.doctorRun },
72
72
  { name: "doctor", args: "--run --since main", text: h.doctorSince },
73
73
  { name: "prove", args: "", text: h.prove },
74
+ { name: "probe", args: "", text: h.probe },
74
75
  { name: "add", args: h.name, text: h.add },
75
76
  { name: "find", args: '"…"', text: h.find },
76
77
  { name: "why", args: '"…"', text: h.why },
@@ -75,10 +75,23 @@ function targetsOf(cmd, isDir) {
75
75
  // не проверен», хотя его обходят пять проверок — они просто промолчали, потому что нашли
76
76
  // чисто. Замеряно 2026-09-08.
77
77
  function coverage(files, results, isDir = () => false) {
78
+ // НАЗВАН — ЭТО ТОЛЬКО ВЫВОД. Пути брались из команды И вывода разом, и гейт, не напечатавший
79
+ // ни слова, делал «названным» любой файл, упомянутый в строке запуска. Опыт 2026-09-09:
80
+ // молчащая проверка `sh checks/quiet.sh .` давала «названо проверкой: checks/quiet.sh».
81
+ // Так «доказывалась» каждая правка `kit/gates/*/check.sh` и `tool/selfcheck/*.sh` — то есть
82
+ // самих проверок, где правда важнее всего.
83
+ //
84
+ // Но и выбросить команду нельзя: она отвечает на ДРУГОЙ вопрос — куда гейт был направлен.
85
+ // Каталог из команды даёт «обойдено молча» через `targets`; отдельный файл раньше не давал
86
+ // ничего, потому что `targetsOf` берёт только каталоги, — и такой файл падал в «никто не
87
+ // смотрел». Поэтому имена из команды хранятся отдельно и сравниваются ТОЧНО.
78
88
  const seen = results.map((r) => ({
79
89
  name: r.name,
80
- paths: pathsIn(`${r.cmd || ""}\n${r.out || ""}`),
90
+ paths: pathsIn(r.out || ""),
81
91
  targets: targetsOf(r.cmd, isDir),
92
+ named: new Set(
93
+ String(r.cmd || "").split(/\s+/).filter((t) => t && !t.startsWith("-")).map(normPath)
94
+ ),
82
95
  }));
83
96
  const covered = new Map();
84
97
  const silent = new Map();
@@ -88,7 +101,7 @@ function coverage(files, results, isDir = () => false) {
88
101
  const by = seen.filter((n) => n.paths.has(f)).map((n) => n.name);
89
102
  if (by.length) { covered.set(raw, by); continue; }
90
103
  const aimed = seen
91
- .filter((n) => n.targets.some((t) => t === "" || f === t || f.startsWith(`${t}/`)))
104
+ .filter((n) => n.named.has(f) || n.targets.some((t) => t === "" || f === t || f.startsWith(`${t}/`)))
92
105
  .map((n) => n.name);
93
106
  if (aimed.length) silent.set(raw, aimed);
94
107
  else uncovered.push(raw);
@@ -0,0 +1,67 @@
1
+ // tool/lib/execution.mjs — ИСХОД ЗАПУСКА процесса, отдельно от смысла находки.
2
+ //
3
+ // ЗАЧЕМ. Прежде `prove.mjs` считал находкой ЛЮБОЙ ненулевой код на красном образце. Опыт
4
+ // 2026-09-09: проверка, которая ВИСНЕТ на красном образце и молчит на зелёном, получала вердикт
5
+ // `proven: 1, ok: true` — зависший гейт объявлялся ловящим брак. Это `pytest || true` в функции,
6
+ // считающей наш главный уровень. Тот же класс поймал перемежающийся отказ прогона: под
7
+ // нагрузкой арбитр записи не успевал, и приёмка печатала «гейт ругается на исправный код» —
8
+ // сбой инструмента выдавался за приговор записи каталога.
9
+ //
10
+ // ТРИ ИСХОДА, А НЕ ДВА: clean · finding · infra_error. Четвёртое состояние — `unavailable`,
11
+ // «инструмента нет» — сюда НЕ входит намеренно: это результат осмотра окружения ДО запуска.
12
+ // Если процесс уже запускался и не смог отработать — это сбой инструмента, а не его отсутствие.
13
+ //
14
+ // ПОЧЕМУ У КАЖДОГО ИНСТРУМЕНТА СВОЙ АДАПТЕР, А НЕ ОДНО ПРАВИЛО. Замер по настоящим
15
+ // инструментам 2026-09-09 (числа сняты запуском, кроме pylint и eslint — их документацией):
16
+ //
17
+ // ruff чисто 0 · находка 1 (синтаксическая ошибка тоже 1) · неверный флаг 2
18
+ // · НЕСУЩЕСТВУЮЩИЙ ПУТЬ → 0 и «All checks passed!»
19
+ // eslint чисто 0 · находка 1 · настройка либо внутренняя ошибка 2
20
+ // vulture чисто 0 · НАХОДКА 3 · плохой ввод 1 · ошибка CLI 2
21
+ // pylint чисто 0 · находка битовой маской (2 ошибка, 4 предупреждение, 8, 16)
22
+ // · ОШИБКА ВЫЗОВА 32
23
+ //
24
+ // Общее правило «1 — находка, 2+ — сбой» переврало бы vulture (его находка это 3, а 1 и 2 —
25
+ // ошибки) и pylint (его ошибка вызова 32 больше любой находки) — причём в ОБЕ стороны.
26
+ // Поэтому знание о кодах живёт рядом с инструментом, а протокол остаётся простым.
27
+
28
+ // Умолчание для незнакомой программы: ноль — чисто, единица — находка, остальное — сбой.
29
+ // Это честнее догадки: неизвестный код становится «не знаем», а не «поймал».
30
+ const DEFAULT = (code) => code === 1;
31
+
32
+ const ADAPTERS = {
33
+ ruff: DEFAULT,
34
+ eslint: DEFAULT,
35
+ // Наши собственные обёртки: 0 чисто, 1 находка, 2 «нет делегированного инструмента».
36
+ bash: DEFAULT,
37
+ sh: DEFAULT,
38
+ vulture: (code) => code === 3,
39
+ // Битовая маска: любое ненулевое БЕЗ бита 32 — находка; бит 32 — ошибка вызова.
40
+ pylint: (code) => code > 0 && (code & 32) === 0,
41
+ };
42
+
43
+ function findingCodes(prog) {
44
+ const key = String(prog || "").split("/").pop();
45
+ return ADAPTERS[key] || DEFAULT;
46
+ }
47
+
48
+ // Вход — то, что отдаёт spawnSync: { status, signal, error }. Выход — исход и, у сбоя, причина.
49
+ function classify(r, isFinding = DEFAULT) {
50
+ // ПОРЯДОК ВАЖЕН, И ОН НЕОЧЕВИДЕН. При истечении срока Node ставит И `signal: SIGTERM`,
51
+ // И `error` с кодом ETIMEDOUT: если смотреть на error первым и не различать его код, таймаут
52
+ // назовётся «ошибкой запуска». Состояние вышло бы верным, а объяснение ложным — а объяснение
53
+ // здесь и есть смысл работы. Замерено прямо на spawnSync 2026-09-09.
54
+ if (r?.error?.code === "ETIMEDOUT") return { state: "infra_error", reason: "timeout", code: null };
55
+ if (r?.error) return { state: "infra_error", reason: "spawn_error", code: null };
56
+ if (r?.status === null || r?.status === undefined) {
57
+ // Срок мог сработать и без error: процесс убит SIGTERM. Иной сигнал — не наш срок.
58
+ const reason = !r?.signal || r.signal === "SIGTERM" ? "timeout" : "signal";
59
+ return { state: "infra_error", reason, code: null };
60
+ }
61
+ const code = r.status;
62
+ if (code === 0) return { state: "clean", reason: null, code };
63
+ if (isFinding(code)) return { state: "finding", reason: null, code };
64
+ return { state: "infra_error", reason: "unexpected_exit", code };
65
+ }
66
+
67
+ export { classify, findingCodes };
@@ -0,0 +1,82 @@
1
+ // tool/lib/history.mjs — что в ЭТОМ репозитории ломается на самом деле.
2
+ //
3
+ // ЗАЧЕМ. `doctor` говорит «держит машина 21». Двадцать один из чего? Знаменателя нет: 21 —
4
+ // это то, что мы успели написать в каталог, а не то, что важно в конкретном проекте. Каталог
5
+ // у нас — наш вкус; история проекта — его факты.
6
+ //
7
+ // Замер 2026-09-09 на самом комплекте: 45 коммитов-починок из 181, и рейтинг однозначный —
8
+ // `tool/selfcheck/smoke.sh` чинили 22 раза, `tool/lib/manifest.mjs` — 5. Это те места, где
9
+ // брак возвращается; там и стоит спрашивать, смотрит ли на них хоть одна проверка.
10
+ //
11
+ // ЧТО ЭТО НЕ ЗНАЧИТ. Часто чинят и то, что часто меняют: рейтинг говорит «сюда возвращаются»,
12
+ // а не «здесь плохо». Ответ на «прикрыто ли» даёт не он, а проба — см. probeVerdict.
13
+
14
+ // Признак починки берётся из ТЕМЫ коммита. Тема — единственное, что пишут все, и единственное,
15
+ // что видно в `git log --oneline`.
16
+ //
17
+ // Три написания, потому что репозитории бывают на двух языках и с conventional commits. Якорь
18
+ // на начало обязателен: без него «feat: prefix для путей» попадает в рейтинг из-за подстроки
19
+ // «fix», и рейтинг перестаёт что-либо значить.
20
+ const FIX_RE = /^\s*(fix|fixes|fixed|bugfix|hotfix|исправ|почин)/i;
21
+
22
+ function isFix(subject) {
23
+ return FIX_RE.test(String(subject || ""));
24
+ }
25
+
26
+ // Разбор вывода `git log --format=%s --name-only`. Формат снят с живого репозитория, а не
27
+ // придуман: ТЕМА, затем ПУСТАЯ строка, затем пути — и сразу следующая тема, без пустой строки
28
+ // перед ней.
29
+ //
30
+ // fix: разбор манифеста
31
+ // <пусто>
32
+ // tool/lib/manifest.mjs
33
+ // feat: новая запись
34
+ // <пусто>
35
+ // kit/gates/x/check.sh
36
+ //
37
+ // Отсюда единственный надёжный признак темы: за ней идёт пустая строка. По позиции её не
38
+ // отличить — после последнего пути соседнего коммита тема начинается без разделителя.
39
+ // Первая версия разбирала по выдуманному формату («пустая строка ПОСЛЕ файлов») и нашла ноль
40
+ // починок там, где их сорок пять: ошибка молчала, потому что пустой рейтинг выглядит как
41
+ // «история чистая». Тот же класс, что дважды поймал меня в тот же день.
42
+ //
43
+ // `isCode` передаётся вызывающим, а не берётся отсюда: набор расширений кода живёт в
44
+ // evidence.mjs, и второй его список через месяц разошёлся бы с первым.
45
+ function fixHotspots(raw, { isCode }) {
46
+ const counts = new Map();
47
+ const lines = String(raw || "").split("\n");
48
+ let inFix = false;
49
+
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i];
52
+ if (line === "") continue;
53
+ if (lines[i + 1] === "") { inFix = isFix(line); continue; }
54
+ if (!inFix) continue;
55
+ const p = line.trim();
56
+ if (!p || !isCode(p)) continue;
57
+ counts.set(p, (counts.get(p) || 0) + 1);
58
+ }
59
+
60
+ return [...counts.entries()]
61
+ .map(([path, fixes]) => ({ path, fixes }))
62
+ // При равном числе починок — по алфавиту: вывод обязан быть одинаковым между прогонами,
63
+ // иначе его нельзя сравнить с прошлым и нельзя проверить машиной.
64
+ .sort((a, b) => b.fixes - a.fixes || a.path.localeCompare(b.path));
65
+ }
66
+
67
+ // Вердикт пробы по кодам возврата объявленных гейтов. ТРИ состояния, и сливать их нельзя.
68
+ //
69
+ // `2` у наших записей означает «проверка не состоялась» — делегированной программы нет на
70
+ // машине. Засчитать это как «прикрыто» значило бы выдать отсутствие сигнала за успех: тот
71
+ // самый отказ, против которого написан весь комплект. Засчитать как «не прикрыто» — тоже
72
+ // неправда: мы не знаем.
73
+ //
74
+ // Поймавший гейт сильнее непроверенного: класс закрыт, даже если рядом чего-то не хватает.
75
+ function probeVerdict(results) {
76
+ if (results.some((r) => r.code === 1)) return "caught";
77
+ if (results.length === 0) return "unknown";
78
+ if (results.some((r) => r.code !== 0)) return "unknown";
79
+ return "blind";
80
+ }
81
+
82
+ export { isFix, fixHotspots, probeVerdict };
@@ -112,7 +112,7 @@ function unparsedLines(text) {
112
112
  // Список обязан совпадать с тем, что программа РЕАЛЬНО читает (`man?.<поле>` в tool/):
113
113
  // лишнее имя здесь молча узаконивает поле, которое ни на что не влияет, — та же тишина,
114
114
  // только с другой стороны. Сверено обходом: aqk, entry, rules, gates, samples, ratchets, lessons.
115
- const KNOWN_KEYS = ["aqk", "entry", "rules", "docs", "lang", "gates", "covers", "samples", "ratchets", "lessons", "advisory"];
115
+ const KNOWN_KEYS = ["aqk", "entry", "rules", "docs", "lang", "gates", "covers", "samples", "ratchets", "lessons", "advisory", "probe"];
116
116
 
117
117
  // ГДЕ У ПРОЕКТА ЛЕЖИТ РАЗЛОЖЕННЫЙ КОМПЛЕКТ. Список для шапки `doctor`. До 2026-09-08 он был
118
118
  // литеральным: `.aqk/rules`, `.aqk/docs`, `AGENTS.md` — независимо от того, что написано в