agent-quality-kit 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/README.ru.md +20 -5
- package/kit/gates/api-contract-has-arbiter/README.md +16 -1
- package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
- package/kit/gates/entry-commands-exist/README.md +64 -0
- package/kit/gates/entry-commands-exist/check.sh +110 -0
- package/kit/gates/entry-commands-exist/gate.yml +19 -0
- package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
- package/kit/gates/entry-commands-exist/green/Makefile +6 -0
- package/kit/gates/entry-commands-exist/green/justfile +2 -0
- package/kit/gates/entry-commands-exist/green/package.json +10 -0
- package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
- package/kit/gates/entry-commands-exist/red/Makefile +2 -0
- package/kit/gates/entry-commands-exist/red/package.json +9 -0
- package/llms.txt +5 -1
- package/package.json +1 -1
- package/tool/commands/context.mjs +34 -15
- package/tool/commands/doctor-catalog.mjs +222 -0
- package/tool/commands/doctor.mjs +28 -211
- package/tool/commands/learn.mjs +119 -19
- package/tool/commands/probe.mjs +4 -2
- package/tool/commands/prompt.mjs +69 -0
- package/tool/i18n/en.mjs +65 -3
- package/tool/i18n/index.mjs +42 -3
- package/tool/i18n/ru.mjs +74 -3
- package/tool/lib/annotate.mjs +66 -0
- package/tool/lib/cadence.mjs +30 -1
- package/tool/lib/core.mjs +1 -0
- package/tool/lib/repo.mjs +45 -4
- package/tool/lib/run.mjs +20 -2
- package/tool/program.mjs +4 -0
- package/tool/selfcheck/smoke/_fixture.mjs +8 -3
- package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
- package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +39 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +41 -2
- package/tool/selfcheck/smoke.sh +4 -0
- package/tool/selfcheck/units-annotate.mjs +67 -0
- package/tool/selfcheck/units-cadence.mjs +26 -1
- package/tool/selfcheck/units-learn.mjs +32 -0
- package/tool/selfcheck/units-level.mjs +21 -1
- package/tool/selfcheck/units-prompt.mjs +106 -0
- package/tool/selfcheck/units-repo.mjs +20 -1
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -16,6 +16,10 @@ set -uo pipefail
|
|
|
16
16
|
# Язык вывода закреплён: проверки ниже сверяют русский текст, а без этой строки они зависели бы
|
|
17
17
|
# от локали машины — на англоязычном раннере зелёное стало бы красным без единой правки в коде.
|
|
18
18
|
export AQK_LANG=ru
|
|
19
|
+
# Полный вывод: проверки этого файла написаны до того, как doctor стал сворачивать пройденное и
|
|
20
|
+
# неприменимое, и ищут строки из этих списков — в том числе ОТСУТСТВИЕ строки, которое на
|
|
21
|
+
# свёрнутом выводе прошло бы вхолостую. Свёртку проверяют файлы smoke/*.mjs, они флаг сбрасывают.
|
|
22
|
+
export AQK_VERBOSE=1
|
|
19
23
|
|
|
20
24
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
21
25
|
CLI="$ROOT/tool/program.mjs"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// tool/selfcheck/units-annotate.mjs — находки гейтов пометками GitHub Actions.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ. Находка гейта видна только в логе конвейера, куда почти никто не заглядывает. Строка
|
|
4
|
+
// `::error file=…,line=…,title=…::…` в выводе шага становится красной пометкой у строки файла в
|
|
5
|
+
// pull request. Идея — из разбора AgentLint (research/competitors/agentlint-0xmariowu.md).
|
|
6
|
+
// Формат — документация GitHub (workflow-commands); экранирование — официальный
|
|
7
|
+
// @actions/core (packages/core/src/command.ts: escapeData, escapeProperty), а не чужой код.
|
|
8
|
+
//
|
|
9
|
+
// node --test tool/selfcheck/units-annotate.mjs
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { locate, annotations } from "../lib/annotate.mjs";
|
|
13
|
+
|
|
14
|
+
const exists = (f) => ["src/a.py", "AGENTS.md", "kit/x.sh"].includes(f);
|
|
15
|
+
|
|
16
|
+
test("locate: «файл:строка: находка» и «файл: находка»; без пути — ничего", () => {
|
|
17
|
+
assert.deepEqual(locate("src/a.py:12: print(x) в прод-коде"), { file: "src/a.py", line: 12, message: "print(x) в прод-коде" });
|
|
18
|
+
assert.deepEqual(locate("./AGENTS.md: «npm run debug» — такого скрипта нет"), { file: "AGENTS.md", line: null, message: "«npm run debug» — такого скрипта нет" });
|
|
19
|
+
assert.deepEqual(locate("src/a.py:3:7: E501 line too long"), { file: "src/a.py", line: 3, message: "E501 line too long" });
|
|
20
|
+
assert.equal(locate("прогон красный: 2 находки"), null);
|
|
21
|
+
assert.equal(locate(" почини: убери print"), null, "совет — не находка");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("упавший гейт — пометка у строки с советом; совещательный — предупреждение", () => {
|
|
25
|
+
const r = annotations([
|
|
26
|
+
{ name: "no-print", ok: false, out: "src/a.py:12: print(x)\n почини: замени на logger\n" },
|
|
27
|
+
{ name: "todo", ok: false, advisory: true, out: "src/a.py:3: TODO без задачи\n" },
|
|
28
|
+
{ name: "lint", ok: true, out: "src/a.py:1: не важно\n" },
|
|
29
|
+
], { exists });
|
|
30
|
+
assert.equal(r.lines.length, 2, "зелёный гейт пометок не даёт");
|
|
31
|
+
assert.equal(r.lines[0], "::error file=src/a.py,line=12,title=aqk%3A no-print::print(x) — почини: замени на logger");
|
|
32
|
+
assert.match(r.lines[1], /^::warning file=src\/a\.py,line=3,/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Пометка на файл, которого нет, — пометка в никуда: GitHub повесит её на `.github`, и человек
|
|
36
|
+
// будет искать то, чего не существует. Такая находка идёт общей пометкой гейта без файла.
|
|
37
|
+
test("путь, которого нет в репозитории, и находка без пути — одна общая пометка гейта", () => {
|
|
38
|
+
const r = annotations([
|
|
39
|
+
{ name: "gates-run-in-ci", ok: false, out: "гейт smoke не запускается конвейером\n" },
|
|
40
|
+
{ name: "ghost", ok: false, out: "vendor/lib.js:4: чужой файл\n" },
|
|
41
|
+
], { exists });
|
|
42
|
+
assert.deepEqual(r.lines, [
|
|
43
|
+
"::error title=aqk%3A gates-run-in-ci::гейт smoke не запускается конвейером",
|
|
44
|
+
"::error title=aqk%3A ghost::vendor/lib.js:4: чужой файл",
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("экранирование по @actions/core: % \\r \\n в тексте, ещё : и , в свойствах", () => {
|
|
49
|
+
const r = annotations([{ name: "a,b", ok: false, out: "src/a.py:1: 100% готово, но\r нет\n" }], { exists });
|
|
50
|
+
assert.equal(r.lines[0], "::error file=src/a.py,line=1,title=aqk%3A a%2Cb::100%25 готово, но%0D нет");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Потолок. В документации GitHub лимита не нашли; в обсуждении сообщества 2020 года (#26680):
|
|
54
|
+
// «10 warning annotations and 10 error annotations per step». Держимся его, остаток — числом.
|
|
55
|
+
test("не больше 10 ошибок и 10 предупреждений; остаток назван числом", () => {
|
|
56
|
+
const out = Array.from({ length: 14 }, (_, i) => `src/a.py:${i + 1}: находка ${i + 1}`).join("\n");
|
|
57
|
+
const r = annotations([{ name: "g", ok: false, out }], { exists });
|
|
58
|
+
assert.equal(r.lines.filter((l) => l.startsWith("::error")).length, 10);
|
|
59
|
+
assert.equal(r.dropped, 4);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Поймано конвейером на первом прогоне: при --since пометки вешались на находки ВНЕ дифа, потому
|
|
63
|
+
// что брались из сырого вывода. Показанное прогоном — единственный источник.
|
|
64
|
+
test("пометки берутся из показанного прогоном, а не из сырого вывода", () => {
|
|
65
|
+
const r = annotations([{ name: "g", ok: false, out: "src/a.py:1: в дифе\nkit/x.sh:2: вне дифа\n", shown: "src/a.py:1: в дифе" }], { exists });
|
|
66
|
+
assert.deepEqual(r.lines, ["::error file=src/a.py,line=1,title=aqk%3A g::в дифе"]);
|
|
67
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// незачем, а сто коммитов за день перепроверить надо.
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { probeDue, probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan, autoProbeAllowed } from "../lib/cadence.mjs";
|
|
10
|
+
import { probeDue, probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan, autoProbeAllowed, parseCounts, levelLimits } from "../lib/cadence.mjs";
|
|
11
11
|
|
|
12
12
|
test("порог по умолчанию — сто коммитов, и он назван числом, а не спрятан", () => {
|
|
13
13
|
assert.equal(PROBE_EVERY, 100);
|
|
@@ -117,3 +117,28 @@ test("проба сама не запускается в конвейере, в
|
|
|
117
117
|
// Явное AQK_PROBE=1 включает и в конвейере: у всего, что решено за человека, есть способ решить иначе.
|
|
118
118
|
assert.equal(autoProbeAllowed({ brief: false, env: { CI: "true", AQK_PROBE: "1" } }), true);
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
// --- чего уровень НЕ доказывает --------------------------------------------------
|
|
122
|
+
// Отзыв с живого проекта 2026-09-11: «после одного --run у нас AQK-3, All levels reached. При
|
|
123
|
+
// этом CI ни разу не запускался, а probe засчитывает всё подряд. Уровень меряет наличие
|
|
124
|
+
// артефактов, а не то, что они работают». Смысл ступеней не меняем — поднимать планку молча
|
|
125
|
+
// значит отнять опубликованный уровень у всех разом; вместо этого под уровнем стоит, чего он
|
|
126
|
+
// не доказывает, и что про это знает последняя проба.
|
|
127
|
+
test("отметка пробы несёт числа пойманных и недоказанных, старая — «не знаем»", () => {
|
|
128
|
+
assert.deepEqual(parseCounts("at: 5\nblind: 0\ncaught: 7\nunknown: 2\n"), { caught: 7, unknown: 2 });
|
|
129
|
+
assert.equal(parseCounts("at: 5\nblind: 0\n"), null, "старая отметка: чисел нет — это не ноль");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("строка про брак в ваших файлах говорит ровно то, что знает проба", () => {
|
|
133
|
+
assert.equal(levelLimits(null).kind, "never");
|
|
134
|
+
assert.equal(levelLimits({ state: "never", behind: null, classes: [], ran: null }).kind, "never");
|
|
135
|
+
assert.equal(levelLimits({ state: "off" }).kind, "off");
|
|
136
|
+
const blind = levelLimits({ state: "fresh", behind: 3, classes: [{ slug: "swallowed-error", file: "a.py" }], ran: new Set(), counts: { caught: 4, unknown: 0 } });
|
|
137
|
+
assert.deepEqual([blind.kind, blind.names, blind.behind], ["blind", ["swallowed-error"], 3], "непойманное — первым, даже если пойманного больше");
|
|
138
|
+
const part = levelLimits({ state: "fresh", behind: 0, classes: [], ran: new Set(), counts: { caught: 4, unknown: 2 } });
|
|
139
|
+
assert.deepEqual([part.kind, part.caught, part.unknown], ["partial", 4, 2]);
|
|
140
|
+
assert.equal(levelLimits({ state: "stale", behind: 150, classes: [], ran: new Set(), counts: { caught: 5, unknown: 0 } }).kind, "caught");
|
|
141
|
+
assert.equal(levelLimits({ state: "fresh", behind: 1, classes: [], ran: new Set(), counts: { caught: 0, unknown: 0 } }).kind, "nothing");
|
|
142
|
+
assert.equal(levelLimits({ state: "fresh", behind: 1, classes: [], ran: new Set(), counts: null }).kind, "old", "старая отметка — «подробности в пробе», а не «всё поймано»");
|
|
143
|
+
assert.equal(levelLimits({ state: "unknown", behind: null, classes: [], ran: new Set(), counts: { caught: 2, unknown: 0 } }).kind, "caught", "без git отметка всё равно читается");
|
|
144
|
+
});
|
|
@@ -86,3 +86,35 @@ test("typedFrom: реплика блоками собирается из тек
|
|
|
86
86
|
});
|
|
87
87
|
assert.equal(typedFrom(line)[0].text, "всегда так");
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
// ПОВТОР — СИГНАЛ, КОТОРЫЙ ДАЁТ САМ ЧЕЛОВЕК. Разбор AgentLint 2026-09-11: их SS2 сопоставляет
|
|
91
|
+
// поправку с правилом свода по словам. Замер на логах владельца: из 45 поправок к записанным
|
|
92
|
+
// правилам относятся от силы две — такой приём дал бы шум. Зато человек сам помечает повтор:
|
|
93
|
+
// «я же говорил», «опять», «снова». Из 14 таких реплик в одном проекте настоящих норм 5–6, и
|
|
94
|
+
// прежний отбор по маркерам наставления не ловил НИ ОДНОЙ.
|
|
95
|
+
test("isRepeat: «я же говорил», «опять», «снова» — повтор; «опять же» и вопрос-недоумение — нет", async () => {
|
|
96
|
+
const { isRepeat } = await import("../commands/learn.mjs");
|
|
97
|
+
assert.equal(isRepeat("нет старые задачи не трогаем я же говорил только пусть новые так работают"), true);
|
|
98
|
+
assert.equal(isRepeat("стой зачем ты сумму убрал я же не просил этого ты опять не верно меня понял"), true);
|
|
99
|
+
assert.equal(isRepeat("ок давай запишем, только опять не хочу плодить файлы"), true);
|
|
100
|
+
assert.equal(isRepeat("I told you not to touch the lockfile"), true);
|
|
101
|
+
assert.equal(isRepeat("и сказать в чате, опять же, где что смотреть"), false, "«опять же» — связка, а не повтор");
|
|
102
|
+
assert.equal(isRepeat("ок го дальше"), false);
|
|
103
|
+
// Второй прогон на тех же логах: половина шума — вопросы-недоумения и вставленные цитаты агента.
|
|
104
|
+
assert.equal(isRepeat("не понял еще нужно что то опять в env добавить?"), false, "вопрос с «опять» — недоумение, а не норма");
|
|
105
|
+
assert.equal(isRepeat("⬜ Два мира ролей — всплывёт, если снова будем трогать роли. поясни"), false, "цитата ответа агента");
|
|
106
|
+
assert.equal(isRepeat("почему ты так долго тестируешь я же говорю не нужно столько тестов?"), true, "сильная пометка держится и с вопросом");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Повтор, совпавший с правилом свода, — главное, что может сказать эта команда: правило
|
|
110
|
+
// записано, а человек всё равно поправляет. Текстом оно не держится — ему нужен сторож.
|
|
111
|
+
test("repeatedRules: повтор, совпавший с правилом свода, называет это правило", async () => {
|
|
112
|
+
const { repeatedRules } = await import("../commands/learn.mjs");
|
|
113
|
+
const entry = "# Правила\n\n- **Не плодить файлы.** Новый файл — только если без него нельзя. <!-- aqk: человек -->\n" +
|
|
114
|
+
"- **Секреты только в .env.** <!-- aqk: secrets-not-in-code -->\n- обычная строка списка без метки\n";
|
|
115
|
+
const hits = repeatedRules([{ text: "опять ты плодишь файлы, я же говорил", when: "2026-09-03" }], entry);
|
|
116
|
+
assert.equal(hits.length, 1);
|
|
117
|
+
assert.match(hits[0].rule, /Не плодить файлы/);
|
|
118
|
+
assert.equal(hits[0].arbiter, "человек");
|
|
119
|
+
assert.equal(repeatedRules([{ text: "опять не тот цвет кнопки", when: "x" }], entry).length, 0, "повтор не про правило — не сюда");
|
|
120
|
+
});
|
|
@@ -11,7 +11,7 @@ import test from "node:test";
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { commandFor, verdict } from "../lib/prove.mjs";
|
|
13
13
|
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
14
|
-
import { pickLang, langFromText } from "../i18n/index.mjs";
|
|
14
|
+
import { pickLang, langFromText, langFromDocs } from "../i18n/index.mjs";
|
|
15
15
|
import { progress, selectGates } from "../lib/run.mjs";
|
|
16
16
|
|
|
17
17
|
// --- строка «идёт» во время прогона ---------------------------------------------------
|
|
@@ -214,6 +214,26 @@ test("мусор в поле lang не молчит, а просто не дей
|
|
|
214
214
|
assert.equal(pickLang({}, { lang: "клингонский" }), "en");
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
// Отзыв с живого проекта 2026-09-11: свод правил и методички на русском, машина на Windows без
|
|
218
|
+
// LANG — и весь вывод английский, пока руками не впишешь `lang:`. Язык текста, который проект
|
|
219
|
+
// сам о себе написал, — свойство проекта; поэтому он выше локали и ниже манифеста.
|
|
220
|
+
test("язык свода проекта сильнее локали и слабее манифеста", () => {
|
|
221
|
+
const ruDoc = "# Правила\n\nПеред коммитом запусти `npm run check` и `make gates`. Схемы живут в " +
|
|
222
|
+
"`packages/contract`, их проверяет сервер.\n\n```sh\nnpm run typecheck && npm run lint\n```\n" +
|
|
223
|
+
"Любое изменение договора проходит через ревью и фиксируется в журнале решений.\n";
|
|
224
|
+
const enDoc = "# Rules\n\nBefore committing run `npm run check`. Schemas live in packages/contract " +
|
|
225
|
+
"and the server validates them. Every contract change goes through review and is logged.\n";
|
|
226
|
+
assert.equal(langFromDocs(ruDoc.repeat(2)), "ru", "русский свод с кодом внутри не узнан");
|
|
227
|
+
assert.equal(langFromDocs(enDoc.repeat(2)), "en");
|
|
228
|
+
assert.equal(langFromDocs("# X\n\n`npm test`\n"), "", "по трём словам язык не решается");
|
|
229
|
+
assert.equal(pickLang({ LANG: "en_US.UTF-8" }, null, "ru"), "ru");
|
|
230
|
+
assert.equal(pickLang({}, null, "ru"), "ru", "Windows без LANG: свод решает");
|
|
231
|
+
assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }, null, "en"), "en");
|
|
232
|
+
assert.equal(pickLang({}, { lang: "en" }, "ru"), "en", "манифест сильнее свода");
|
|
233
|
+
assert.equal(pickLang({ AQK_LANG: "en" }, null, "ru"), "en", "переменная сильнее свода");
|
|
234
|
+
assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }, null, ""), "ru", "свод молчит — решает локаль");
|
|
235
|
+
});
|
|
236
|
+
|
|
217
237
|
// Сокращённый разбор языка в i18n/index.mjs существует потому, что каталог строк нужен раньше,
|
|
218
238
|
// чем кто-либо успеет прочитать манифест целиком. Два разбора одного файла — то же, что два
|
|
219
239
|
// свода правил: через месяц они расходятся, и непонятно, какой настоящий. Сверяем ответы.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// tool/selfcheck/units-prompt.mjs — задание для агента одним текстом (`aqk prompt`).
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ. Между диагнозом и действием не было моста: `doctor` пишет человеку, `context` — «как
|
|
4
|
+
// дела» агенту, а «почини вот это, это и это» человек пересказывал сам. Идея — из разбора
|
|
5
|
+
// agentlint (research/competitors/agentlint.md, «Задание для агента»): правила поведения
|
|
6
|
+
// сверху, исправления по весу, в конце — как проверить. Их слабость не берём: у них «проверь» —
|
|
7
|
+
// это «балл вырос», у нас — команда, которая краснеет и зеленеет.
|
|
8
|
+
//
|
|
9
|
+
// node --test tool/selfcheck/units-prompt.mjs
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { taskText } from "../commands/prompt.mjs";
|
|
13
|
+
import { CATALOGS } from "../i18n/index.mjs";
|
|
14
|
+
|
|
15
|
+
const T = CATALOGS.ru.prompt;
|
|
16
|
+
const base = {
|
|
17
|
+
self: "aqk", manifest: true, run: { when: "2026-09-11 12:00", red: [], stale: false },
|
|
18
|
+
blind: [], adopt: [], shim: null, start: [],
|
|
19
|
+
};
|
|
20
|
+
const text = (s) => taskText({ ...base, ...s }, T).join("\n");
|
|
21
|
+
const items = (s) => text(s).split("\n").filter((l) => /^\d+\. /.test(l));
|
|
22
|
+
|
|
23
|
+
test("правила поведения сверху и проверка в конце — всегда, даже когда пунктов нет", () => {
|
|
24
|
+
const t = text({});
|
|
25
|
+
assert.ok(t.indexOf(T.rulesTitle) < t.indexOf(T.verifyTitle), "правила идут до проверки");
|
|
26
|
+
assert.match(t, /aqk doctor --run/, "без команды проверки «готово» ничем не доказано");
|
|
27
|
+
assert.match(t, new RegExp(T.empty.slice(0, 20)), "пустое задание сказано словами");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("красный гейт — пункт с командой, которая его запускает одного", () => {
|
|
31
|
+
const it = items({ run: { when: "x", red: ["lint", "units"], stale: false } });
|
|
32
|
+
assert.equal(it.length, 2);
|
|
33
|
+
assert.match(it[0], /aqk doctor --run --only lint/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Прогона не было или он старше последнего коммита: список красных — про другой код. Первый
|
|
37
|
+
// пункт — прогнать; пустое задание при этом было бы утверждением «чисто», которого никто не делал.
|
|
38
|
+
test("нет прогона или он устарел — первый пункт «прогони», а не «всё чисто»", () => {
|
|
39
|
+
const none = text({ run: null });
|
|
40
|
+
assert.doesNotMatch(none, new RegExp(T.empty.slice(0, 20)), "без прогона задание не пустое");
|
|
41
|
+
assert.match(items({ run: null })[0], /aqk doctor --run/);
|
|
42
|
+
const stale = items({ run: { when: "x", red: ["lint"], stale: true } });
|
|
43
|
+
assert.match(stale[0], /aqk doctor --run/, "устаревший прогон — сначала прогнать заново");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("порядок по весу: манифест → красное → брак из пробы → свои проверки → свод → начало", () => {
|
|
47
|
+
const it = items({
|
|
48
|
+
manifest: false,
|
|
49
|
+
run: { when: "x", red: ["lint"], stale: false },
|
|
50
|
+
blind: [{ slug: "swallowed-error", file: "src/a.py", command: "ruff check --select BLE ." }],
|
|
51
|
+
adopt: [{ name: "test", cmd: "npm test", source: "package.json" }],
|
|
52
|
+
shim: "missing",
|
|
53
|
+
start: [{ slug: "secrets-not-in-code", intent: "ключи не в коде", command: "gitleaks dir ." }],
|
|
54
|
+
});
|
|
55
|
+
assert.match(it[0], /aqk init/);
|
|
56
|
+
assert.match(it[1], /lint/);
|
|
57
|
+
assert.match(it[2], /swallowed-error.*src\/a\.py/);
|
|
58
|
+
assert.match(it[3], /npm test/);
|
|
59
|
+
assert.match(it[4], /@AGENTS\.md/);
|
|
60
|
+
assert.equal(it.length, 5, "больше пяти за раз агент не удержит");
|
|
61
|
+
assert.match(text({
|
|
62
|
+
manifest: false, run: { when: "x", red: ["lint"], stale: false },
|
|
63
|
+
blind: [{ slug: "s", file: "f", command: "" }], adopt: [{ name: "t", cmd: "c", source: "p" }],
|
|
64
|
+
shim: "missing", start: [{ slug: "x", intent: "y", command: "" }],
|
|
65
|
+
}), new RegExp(T.more(1).slice(0, 12)), "остаток назван числом, а не выброшен молча");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("класс из пробы не повторяется в «начните с»; без готовой команды — aqk add", () => {
|
|
69
|
+
const it = items({
|
|
70
|
+
blind: [{ slug: "swallowed-error", file: "src/a.py", command: "" }],
|
|
71
|
+
start: [{ slug: "swallowed-error", intent: "i", command: "" }, { slug: "todo-without-task", intent: "i", command: "" }],
|
|
72
|
+
});
|
|
73
|
+
assert.equal(it.length, 2);
|
|
74
|
+
assert.match(it[0], /aqk add swallowed-error/);
|
|
75
|
+
assert.match(it[1], /todo-without-task/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Гейт стоит, проба его гоняла — и он пропустил. Не «поставь» (стоит), а «здесь он слеп».
|
|
79
|
+
test("гейт стоит, но пропустил брак из пробы — пункт «разберись», сразу после красных", () => {
|
|
80
|
+
const it = items({
|
|
81
|
+
run: { when: "x", red: ["lint"], stale: false },
|
|
82
|
+
missed: [{ slug: "swallowed-error", file: "src/a.py" }],
|
|
83
|
+
start: [{ slug: "todo-without-task", intent: "i", command: "" }],
|
|
84
|
+
});
|
|
85
|
+
assert.match(it[1], /swallowed-error.*src\/a\.py/);
|
|
86
|
+
assert.doesNotMatch(it[1], /aqk add/, "гейт уже стоит — ставить его второй раз бессмысленно");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("у каждого пункта — чем доказать, что готово", () => {
|
|
90
|
+
const it = items({
|
|
91
|
+
run: { when: "x", red: ["lint"], stale: false },
|
|
92
|
+
blind: [{ slug: "swallowed-error", file: "src/a.py", command: "" }],
|
|
93
|
+
missed: [{ slug: "complexity-limit", file: "src/b.py" }],
|
|
94
|
+
start: [{ slug: "todo-without-task", intent: "i", command: "" }],
|
|
95
|
+
});
|
|
96
|
+
for (const line of it) assert.match(line, new RegExp(T.done.slice(0, 6)), `пункт без арбитра: ${line}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("оба языка несут одни и те же ключи задания", () => {
|
|
100
|
+
const keys = (o) => Object.keys(o).sort().join(",");
|
|
101
|
+
assert.equal(keys(CATALOGS.ru.prompt), keys(CATALOGS.en.prompt));
|
|
102
|
+
// Пункты лежат глубже — пропущенный в одном языке пункт упал бы ошибкой только у того, кто
|
|
103
|
+
// на этом языке работает.
|
|
104
|
+
assert.equal(keys(CATALOGS.ru.prompt.item), keys(CATALOGS.en.prompt.item));
|
|
105
|
+
assert.equal(keys(CATALOGS.ru.prompt.item.shim), keys(CATALOGS.en.prompt.item.shim));
|
|
106
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// node --test tool/selfcheck/units-repo.mjs
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
|
-
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec } from "../lib/repo.mjs";
|
|
11
|
+
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec, claudeSeesRules } from "../lib/repo.mjs";
|
|
12
12
|
import { startWith } from "../lib/advice.mjs";
|
|
13
13
|
import { proposeGates } from "../lib/adopt.mjs";
|
|
14
14
|
import { CATALOGS, L } from "../i18n/index.mjs";
|
|
@@ -308,3 +308,22 @@ test("совет проекту на Biome: команда Biome, а не eslint
|
|
|
308
308
|
const none = blindAdvice({ ...entry, biome_rules: "none" }, { langs: new Set(["javascript"]), has_biome: true });
|
|
309
309
|
assert.match(none.command, /^eslint /);
|
|
310
310
|
});
|
|
311
|
+
|
|
312
|
+
// --- свод виден Claude Code --------------------------------------------------------
|
|
313
|
+
// Документация Claude Code (code.claude.com/docs/en/memory, раздел AGENTS.md, сверено
|
|
314
|
+
// 2026-09-11): «Claude Code reads CLAUDE.md, not AGENTS.md». Рекомендовано: CLAUDE.md с
|
|
315
|
+
// @AGENTS.md либо символическая ссылка. Проект, где Claude Code настроен, а свод лежит только
|
|
316
|
+
// в AGENTS.md, пишет правила агенту, который их не читает.
|
|
317
|
+
test("свод в AGENTS.md виден Claude Code только через CLAUDE.md с @AGENTS.md", () => {
|
|
318
|
+
const base = { agents: true, claude: null, claudeLink: false, dotClaude: false };
|
|
319
|
+
assert.equal(claudeSeesRules(base), null, "Claude Code не настроен — молчим: Codex и Cursor читают AGENTS.md сами");
|
|
320
|
+
assert.equal(claudeSeesRules({ ...base, dotClaude: true }), "missing");
|
|
321
|
+
assert.equal(claudeSeesRules({ ...base, claude: "# Rules\n\nSee AGENTS.md for everything.\n" }), "noImport",
|
|
322
|
+
"упоминание словами — не подключение: файл Claude Code в контекст не загрузит");
|
|
323
|
+
assert.equal(claudeSeesRules({ ...base, claude: "# CLAUDE.md\n\n@AGENTS.md\n" }), null);
|
|
324
|
+
assert.equal(claudeSeesRules({ ...base, claude: "@../AGENTS.md\n" }), null, "из .claude/CLAUDE.md свод подключают на уровень выше");
|
|
325
|
+
assert.equal(claudeSeesRules({ ...base, claude: "Правила — в `@AGENTS.md`.\n" }), "noImport",
|
|
326
|
+
"в обратных кавычках @ не подключает — так в документации");
|
|
327
|
+
assert.equal(claudeSeesRules({ ...base, claude: "x", claudeLink: true }), null, "ссылка на AGENTS.md — тот же файл");
|
|
328
|
+
assert.equal(claudeSeesRules({ ...base, agents: false, dotClaude: true }), null, "AGENTS.md нет — подключать нечего");
|
|
329
|
+
});
|