agent-quality-kit 0.7.0 → 0.8.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 +45 -2
- package/README.ru.md +45 -2
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +6 -2
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/complexity-limit/check.sh +6 -7
- package/kit/gates/duplicate-code/check.sh +5 -1
- package/kit/gates/entry-links-exist/check.sh +4 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
- package/kit/gates/file-size-limit/check.sh +1 -1
- package/kit/gates/secrets-not-in-code/check.sh +16 -3
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/todo-without-task/check.sh +1 -1
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/llms.txt +22 -1
- package/package.json +4 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +25 -18
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +1 -0
- package/tool/commands/report.mjs +33 -1
- package/tool/i18n/en-docs.mjs +85 -1
- package/tool/i18n/en.mjs +21 -36
- package/tool/i18n/ru-docs.mjs +87 -1
- package/tool/i18n/ru.mjs +21 -36
- package/tool/lib/core.mjs +30 -1
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +29 -2
- package/tool/lib/prove.mjs +13 -1
- package/tool/lib/scope.mjs +10 -1
- package/tool/lib/templates.mjs +1 -0
- package/tool/program.mjs +19 -23
- package/tool/selfcheck/smoke.sh +242 -2
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +65 -3
- package/tool/selfcheck/units.mjs +1 -0
package/tool/lib/scope.mjs
CHANGED
|
@@ -33,6 +33,15 @@ const ANSI = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
|
|
|
33
33
|
// принималась бы за находку и отбрасывалась.
|
|
34
34
|
const CANDIDATE = /[\w.@+-]+(?:\/[\w.@+-]+)*\.[A-Za-z][A-Za-z0-9]{0,9}/g;
|
|
35
35
|
|
|
36
|
+
// Все пути, названные в тексте. Тот же разбор, что при сужении: цвет снимается, вид пути
|
|
37
|
+
// нормализуется. Вынесено отдельно, потому что покрытие спрашивает у вывода обратное:
|
|
38
|
+
// не «попадает ли находка в диф», а «назвал ли гейт этот файл».
|
|
39
|
+
function pathsIn(text) {
|
|
40
|
+
const out = new Set();
|
|
41
|
+
for (const m of String(text).replace(ANSI, "").matchAll(CANDIDATE)) out.add(normPath(m[0]));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function inScope(candidate, files) {
|
|
37
46
|
const c = normPath(candidate);
|
|
38
47
|
if (files.has(c)) return true;
|
|
@@ -128,4 +137,4 @@ function changedFiles(ref, cwd) {
|
|
|
128
137
|
);
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
export { scopeOutput, splitAdvice, changedFiles };
|
|
140
|
+
export { scopeOutput, splitAdvice, changedFiles, pathsIn, normPath };
|
package/tool/lib/templates.mjs
CHANGED
package/tool/program.mjs
CHANGED
|
@@ -16,14 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import { realpathSync } from "node:fs";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { c, SELF } from "./lib/core.mjs";
|
|
19
|
+
import { c, SELF, commandRows } from "./lib/core.mjs";
|
|
20
20
|
import { L } from "./i18n/index.mjs";
|
|
21
21
|
import { cmdInit, cmdNote, cmdBlob, cmdStart } from "./commands/project.mjs";
|
|
22
22
|
import { cmdDoctor } from "./commands/doctor.mjs";
|
|
23
23
|
import { cmdAdd, cmdNew, cmdRatchet, cmdFind, cmdWhy } from "./commands/gates.mjs";
|
|
24
24
|
import { cmdReport } from "./commands/report.mjs";
|
|
25
|
+
import { cmdLearn } from "./commands/learn.mjs";
|
|
25
26
|
import { cmdBadge } from "./commands/badge.mjs";
|
|
26
27
|
import { cmdProve } from "./commands/prove.mjs";
|
|
28
|
+
import { cmdContext } from "./commands/context.mjs";
|
|
27
29
|
|
|
28
30
|
// Разбор аргументов выполняется только при запуске файла как программы. При импорте —
|
|
29
31
|
// а так его читают модульные проверки tool/selfcheck/units.mjs — CLI запускаться не должен.
|
|
@@ -67,46 +69,40 @@ if (IS_MAIN) {
|
|
|
67
69
|
case "blob":
|
|
68
70
|
await cmdBlob();
|
|
69
71
|
break;
|
|
72
|
+
// Читает локальную переписку — поэтому только в терминал и всегда с кодом 0. Подробности
|
|
73
|
+
// и замер, на котором стоит отбор, — в шапке tool/commands/learn.mjs.
|
|
74
|
+
case "learn":
|
|
75
|
+
await cmdLearn();
|
|
76
|
+
break;
|
|
77
|
+
|
|
70
78
|
case "report":
|
|
71
79
|
await cmdReport();
|
|
72
80
|
break;
|
|
73
81
|
case "prove":
|
|
74
82
|
await cmdProve();
|
|
75
83
|
break;
|
|
84
|
+
// Печатает состояние репозитория для КОНТЕКСТА агента, а не для человека. Зовётся хуком
|
|
85
|
+
// SessionStart, поэтому ничего не запускает и всегда выходит с нулём: хук, роняющий запуск
|
|
86
|
+
// агента из-за неготового проекта, отключат в тот же день, и не станет ни хука, ни блока.
|
|
87
|
+
case "context":
|
|
88
|
+
await cmdContext(rest);
|
|
89
|
+
break;
|
|
76
90
|
case "badge":
|
|
77
91
|
await cmdBadge(rest);
|
|
78
92
|
break;
|
|
79
93
|
default: {
|
|
80
94
|
// Ширина колонки считается, а не подбирается пробелами: строки в двух языках разной
|
|
81
95
|
// длины, и вручную выровненная справка на втором языке разъезжается.
|
|
82
|
-
const
|
|
83
|
-
const rows = [
|
|
84
|
-
[`${SELF} init`, h.init],
|
|
85
|
-
[`${SELF} init --force`, h.initForce],
|
|
86
|
-
[`${SELF} start`, h.start],
|
|
87
|
-
[`${SELF} doctor`, h.doctor],
|
|
88
|
-
[`${SELF} doctor --run`, h.doctorRun],
|
|
89
|
-
[`${SELF} doctor --run --since main`, h.doctorSince],
|
|
90
|
-
[`${SELF} prove`, h.prove],
|
|
91
|
-
[`${SELF} add ${h.name}`, h.add],
|
|
92
|
-
[`${SELF} find "…"`, h.find],
|
|
93
|
-
[`${SELF} why "…"`, h.why],
|
|
94
|
-
[`${SELF} ratchet ${h.name}`, h.ratchet],
|
|
95
|
-
[`${SELF} new ${h.name}`, h.new],
|
|
96
|
-
[`${SELF} note "…"`, h.note],
|
|
97
|
-
[`${SELF} blob`, h.blob],
|
|
98
|
-
[`${SELF} report`, h.report],
|
|
99
|
-
[`${SELF} badge`, h.badge],
|
|
100
|
-
];
|
|
96
|
+
const rows = commandRows(L).map((r) => [`${SELF} ${r.name}${r.args ? " " + r.args : ""}`, r.text]);
|
|
101
97
|
const width = Math.max(...rows.map(([cmdText]) => cmdText.length));
|
|
102
98
|
const lines = rows.map(([cmdText, text]) => ` ${c.bold(cmdText.padEnd(width))} ${text}`);
|
|
103
99
|
console.log(`
|
|
104
|
-
${c.bold("aqk")} — ${
|
|
100
|
+
${c.bold("aqk")} — ${L.help.tagline}
|
|
105
101
|
|
|
106
102
|
${lines.join("\n")}
|
|
107
103
|
|
|
108
|
-
${c.dim(
|
|
109
|
-
${c.dim(
|
|
104
|
+
${c.dim(L.help.noInstall)}
|
|
105
|
+
${c.dim(L.help.language)}
|
|
110
106
|
`);
|
|
111
107
|
process.exit(cmd ? 1 : 0);
|
|
112
108
|
}
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -25,6 +25,13 @@ FAIL=0
|
|
|
25
25
|
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS + 1)); }
|
|
26
26
|
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}"; FAIL=$((FAIL + 1)); }
|
|
27
27
|
|
|
28
|
+
# Node на Windows видит мир глазами Windows, а Git Bash — глазами POSIX: путь вида
|
|
29
|
+
# /tmp/tmp.XXXX, отданный в `node -e`, там не существует, и проверка падает не на том, что
|
|
30
|
+
# проверяет. Поймано дважды на windows-прогоне — сперва на `learn`, потом на установке хука.
|
|
31
|
+
# Поэтому node запускается ИЗ каталога и получает относительный путь: помощник, а не памятка,
|
|
32
|
+
# потому что памятку третий раз забудут ровно так же, как забыли второй.
|
|
33
|
+
node_in() { D="$1"; shift; ( cd "$D" && node "$@" ); }
|
|
34
|
+
|
|
28
35
|
WORK="$(mktemp -d)"
|
|
29
36
|
trap 'rm -rf "$WORK"' EXIT
|
|
30
37
|
|
|
@@ -1183,7 +1190,11 @@ rm -rf "$VDIR"
|
|
|
1183
1190
|
# v0.4.2, отставание на два выпуска. Проверка сравнивает обе с package.json, а не друг с другом:
|
|
1184
1191
|
# совпасть друг с другом они могут и будучи одинаково устаревшими.
|
|
1185
1192
|
VERS_BAD=""
|
|
1186
|
-
|
|
1193
|
+
# README.ru.md попал сюда 2026-09-08, при выпуске 0.8.0: в нём стояли v0.6.0 и v0.5.0 —
|
|
1194
|
+
# отставание на два и на три выпуска. Проверка его не смотрела, и русский читатель ставил
|
|
1195
|
+
# прошлогодний комплект молча. Ровно та же дыра, ради которой проверку и заводили, — просто
|
|
1196
|
+
# в файле, который забыли перечислить.
|
|
1197
|
+
for VF in README.md README.ru.md llms.txt; do
|
|
1187
1198
|
[ -f "$ROOT/$VF" ] || continue
|
|
1188
1199
|
# Берём только версии AQK — «v1.2.3» в примерах чужих действий (actions/checkout@v4) не наши.
|
|
1189
1200
|
for V in $(grep -oE '(rev:[[:space:]]*|Agent_Quality_Kit@)v[0-9]+\.[0-9]+\.[0-9]+' "$ROOT/$VF" \
|
|
@@ -1192,11 +1203,240 @@ for VF in README.md llms.txt; do
|
|
|
1192
1203
|
done
|
|
1193
1204
|
done
|
|
1194
1205
|
if [ -z "$VERS_BAD" ]; then
|
|
1195
|
-
ok "версия в README и llms.txt совпадает с package.json (v$PKGVER)"
|
|
1206
|
+
ok "версия в обоих README и llms.txt совпадает с package.json (v$PKGVER)"
|
|
1196
1207
|
else
|
|
1197
1208
|
bad "версия в документах разошлась с package.json" "package.json: v$PKGVER; найдено — $VERS_BAD"
|
|
1198
1209
|
fi
|
|
1199
1210
|
|
|
1211
|
+
# --- 82. report --since называет, чем доказан диф ---------------------------
|
|
1212
|
+
# Три состояния у файла, и разница между вторым и третьим — весь смысл раздела: «проверка
|
|
1213
|
+
# обошла и промолчала» не то же самое, что «никто не смотрел». Двух состояний хватило ровно до
|
|
1214
|
+
# первого прогона — `tool/commands/doctor.mjs` попал в «никем не проверен», хотя его обходят
|
|
1215
|
+
# пять проверок; они молчали, потому что нашли чисто.
|
|
1216
|
+
EVDIR="$(mktemp -d)"
|
|
1217
|
+
(
|
|
1218
|
+
cd "$EVDIR" || exit 1
|
|
1219
|
+
git init -q . && git config user.email a@b.c && git config user.name t
|
|
1220
|
+
mkdir -p src gates/noisy
|
|
1221
|
+
printf 'x = 1\n' > src/kept.py
|
|
1222
|
+
# Гейт, который печатает путь: по нему файл становится «назван».
|
|
1223
|
+
printf '#!/usr/bin/env sh\necho "src/kept.py:1: нашёл"\nexit 0\n' > gates/noisy/check.sh
|
|
1224
|
+
printf 'aqk: "1"\nentry: [AGENTS.md]\ngates:\n noisy: "sh gates/noisy/check.sh ."\n' > .aqk.yml
|
|
1225
|
+
printf '# правила\n' > AGENTS.md
|
|
1226
|
+
git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1
|
|
1227
|
+
printf 'y = 2\n' > src/quiet.py
|
|
1228
|
+
git add -A >/dev/null 2>&1 && git commit -qm second >/dev/null 2>&1
|
|
1229
|
+
) >/dev/null 2>&1
|
|
1230
|
+
EV_OUT=$( cd "$EVDIR" && AQK_LANG=ru node "$CLI" report --since HEAD~1 2>&1 )
|
|
1231
|
+
EV_FILE="$EVDIR/.aqk/report.md"
|
|
1232
|
+
if [ -f "$EV_FILE" ] && grep -q "src/quiet.py" "$EV_FILE" && grep -q "Чем доказан" "$EV_FILE"; then
|
|
1233
|
+
ok "report --since называет, чем доказан диф"
|
|
1234
|
+
else
|
|
1235
|
+
bad "report --since не отчитался о покрытии" "$(printf '%s' "$EV_OUT" | tail -2 | head -1)"
|
|
1236
|
+
fi
|
|
1237
|
+
# Ссылка, которой нет, обязана быть названа: «сравнили не с тем» не должно читаться как «чисто».
|
|
1238
|
+
EV_BAD=$( cd "$EVDIR" && AQK_LANG=ru node "$CLI" report --since no-such-ref 2>&1 )
|
|
1239
|
+
if printf '%s' "$EV_BAD" | grep -q "no-such-ref"; then
|
|
1240
|
+
ok "report --since называет неразобранную ссылку"
|
|
1241
|
+
else
|
|
1242
|
+
bad "report --since проглотил неверную ссылку" "$(printf '%s' "$EV_BAD" | tail -2 | head -1)"
|
|
1243
|
+
fi
|
|
1244
|
+
rm -rf "$EVDIR"
|
|
1245
|
+
|
|
1246
|
+
# --- 84. learn читает только напечатанное человеком --------------------------
|
|
1247
|
+
# Поле promptSource отделяет реплику от результата инструмента. Без него первая версия отбора
|
|
1248
|
+
# выдавала вставленные пути и ссылки вместо правил — «agent quality kit» 44 раза.
|
|
1249
|
+
# Каталог логов кладём ВНУТРЬ проекта и передаём относительным путём. Абсолютный сюда не
|
|
1250
|
+
# годится: Git Bash на Windows отдаёт «/tmp/…», а Node в том же окружении читает это как
|
|
1251
|
+
# «C:\tmp\…» — переменная указывает в никуда, и проверка падает не по делу.
|
|
1252
|
+
LRNP="$(mktemp -d)"; LRN=".cfg"
|
|
1253
|
+
# Имя каталога логов спрашиваем у самой программы, а не считаем здесь. На Windows оболочка
|
|
1254
|
+
# отдаёт «/tmp/…», а Node видит «C:\Users\…» — две разные строки, и тест падал не по делу.
|
|
1255
|
+
# Правило перевода пути в имя сторожит модульная проверка logSlug, здесь проверяется конвейер.
|
|
1256
|
+
SLUG=$( cd "$LRNP" && node -e "const {pathToFileURL}=require('node:url');import(pathToFileURL(process.argv[1]).href).then(m=>console.log(m.logSlug(process.cwd())))" "$ROOT/tool/commands/learn.mjs" )
|
|
1257
|
+
mkdir -p "$LRNP/$LRN/projects/$SLUG"
|
|
1258
|
+
{
|
|
1259
|
+
printf '{"type":"user","promptSource":"typed","timestamp":"2026-09-08T10:00:00Z","message":{"role":"user","content":"никогда не коммить прямо в основную ветку"}}\n'
|
|
1260
|
+
printf '{"type":"user","promptSource":"typed","timestamp":"2026-09-08T10:01:00Z","message":{"role":"user","content":"ок го дальше"}}\n'
|
|
1261
|
+
printf '{"type":"user","timestamp":"2026-09-08T10:02:00Z","message":{"role":"user","content":[{"type":"tool_result","content":"нельзя обязательно всегда"}]}}\n'
|
|
1262
|
+
} > "$LRNP/$LRN/projects/$SLUG/s1.jsonl"
|
|
1263
|
+
printf '# правила\n- Ничего особенного.\n' > "$LRNP/AGENTS.md"
|
|
1264
|
+
printf 'aqk: "1"\nentry: [AGENTS.md]\n' > "$LRNP/.aqk.yml"
|
|
1265
|
+
LRN_OUT=$( cd "$LRNP" && CLAUDE_CONFIG_DIR="$LRN" AQK_LANG=ru node "$CLI" learn 2>&1 )
|
|
1266
|
+
if printf '%s' "$LRN_OUT" | grep -q "основную ветку" &&
|
|
1267
|
+
! printf '%s' "$LRN_OUT" | grep -q "го дальше" &&
|
|
1268
|
+
printf '%s' "$LRN_OUT" | grep -q "напечатано человеком: 2"; then
|
|
1269
|
+
ok "learn берёт напечатанное человеком и не берёт вывод инструментов"
|
|
1270
|
+
else
|
|
1271
|
+
bad "learn отобрал не то" "$(printf '%s' "$LRN_OUT" | tr '\n' ' ' | cut -c1-150)"
|
|
1272
|
+
fi
|
|
1273
|
+
# Правило, уже стоящее в точке входа, показывать незачем: команда не пересказывает свод.
|
|
1274
|
+
printf '# правила\n- Никогда не коммить прямо в основную ветку.\n' > "$LRNP/AGENTS.md"
|
|
1275
|
+
LRN_W=$( cd "$LRNP" && CLAUDE_CONFIG_DIR="$LRN" AQK_LANG=ru node "$CLI" learn 2>&1 )
|
|
1276
|
+
if printf '%s' "$LRN_W" | grep -q "уже стоит в точке входа"; then
|
|
1277
|
+
ok "learn молчит о правиле, которое уже записано"
|
|
1278
|
+
else
|
|
1279
|
+
bad "learn повторил записанное правило" "$(printf '%s' "$LRN_W" | tr '\n' ' ' | cut -c1-150)"
|
|
1280
|
+
fi
|
|
1281
|
+
# Читает переписку — значит на диск не пишет ничего. Проверяем буквально.
|
|
1282
|
+
if [ ! -d "$LRNP/.aqk" ] || [ -z "$(ls -A "$LRNP/.aqk" 2>/dev/null)" ]; then
|
|
1283
|
+
ok "learn ничего не записал на диск"
|
|
1284
|
+
else
|
|
1285
|
+
bad "learn создал файлы" "$(ls -A "$LRNP/.aqk" | tr '\n' ' ')"
|
|
1286
|
+
fi
|
|
1287
|
+
rm -rf "$LRNP"
|
|
1288
|
+
|
|
1289
|
+
# --- 87. --baseline не совмещается с --run и --min ---------------------------
|
|
1290
|
+
# `--baseline` выходит с нулём всегда: это осмотр, а не прогон. Совмещённый с порогом он давал
|
|
1291
|
+
# конвейер, который НЕ МОЖЕТ покраснеть — порог назван, гейты не запущены, код нулевой. Найдено
|
|
1292
|
+
# ревью 2026-09-08. Отказ должен быть громким: молчаливое зелёное здесь дороже сломанной команды.
|
|
1293
|
+
BLP="$(mktemp -d)"
|
|
1294
|
+
( cd "$BLP" && git init -q . && printf 'aqk: "1"\nentry: [AGENTS.md]\n' > .aqk.yml && printf '# правила\n' > AGENTS.md ) >/dev/null 2>&1
|
|
1295
|
+
BL_OK=$( cd "$BLP" && AQK_LANG=ru node "$CLI" doctor --baseline 2>&1 ); BL_OK_C=$?
|
|
1296
|
+
BL_BAD=$( cd "$BLP" && AQK_LANG=ru node "$CLI" doctor --baseline --min 1 2>&1 ); BL_BAD_C=$?
|
|
1297
|
+
BL_RUN=$( cd "$BLP" && AQK_LANG=ru node "$CLI" doctor --baseline --run 2>&1 ); BL_RUN_C=$?
|
|
1298
|
+
if [ "$BL_OK_C" -eq 0 ] && [ "$BL_BAD_C" -ne 0 ] && [ "$BL_RUN_C" -ne 0 ] &&
|
|
1299
|
+
printf '%s' "$BL_BAD" | grep -q "не может покраснеть"; then
|
|
1300
|
+
ok "--baseline с --min и --run отказывает вслух, сам по себе работает"
|
|
1301
|
+
else
|
|
1302
|
+
bad "--baseline не отказал на пороге" "коды: сам $BL_OK_C, с --min $BL_BAD_C, с --run $BL_RUN_C"
|
|
1303
|
+
fi
|
|
1304
|
+
rm -rf "$BLP"
|
|
1305
|
+
|
|
1306
|
+
# --- 88. записи каталога не ломаются от цвета в выводе арбитра ---------------
|
|
1307
|
+
# Внутри GitHub Actions чужие инструменты КРАСЯТ вывод: там цвет поддержан, вне конвейера они
|
|
1308
|
+
# его выключают сами. Разбор по началу строки тогда промахивается — строка начинается с
|
|
1309
|
+
# escape-последовательности. Поймано конвейером 2026-09-08 на записи ci-not-hijackable: локально
|
|
1310
|
+
# зелёная, в конвейере «формат сменился». Тот же класс уже записан в scope.mjs.
|
|
1311
|
+
CLR_BAD=""
|
|
1312
|
+
for CG in kit/gates/*/check.sh; do
|
|
1313
|
+
CS=$(basename "$(dirname "$CG")")
|
|
1314
|
+
# Только записи с готовым арбитром: у них вывод чужой, и красит его не наш код.
|
|
1315
|
+
grep -q "^requires:" "$(dirname "$CG")/gate.yml" 2>/dev/null || continue
|
|
1316
|
+
command -v "$(sed -n 's/^requires:[[:space:]]*//p' "$(dirname "$CG")/gate.yml" | head -1 | awk '{print $1}')" >/dev/null 2>&1 || continue
|
|
1317
|
+
PLAIN=$(bash "$CG" "$(dirname "$CG")/green" >/dev/null 2>&1; echo $?)
|
|
1318
|
+
COLOR=$(GITHUB_ACTIONS=true CI=true bash "$CG" "$(dirname "$CG")/green" >/dev/null 2>&1; echo $?)
|
|
1319
|
+
[ "$PLAIN" = "$COLOR" ] || CLR_BAD="${CLR_BAD:+$CLR_BAD, }$CS ($PLAIN vs $COLOR)"
|
|
1320
|
+
done
|
|
1321
|
+
if [ -z "$CLR_BAD" ]; then
|
|
1322
|
+
ok "вердикт записи не зависит от того, красит ли арбитр вывод"
|
|
1323
|
+
else
|
|
1324
|
+
bad "цвет в выводе арбитра меняет вердикт" "$CLR_BAD"
|
|
1325
|
+
fi
|
|
1326
|
+
|
|
1327
|
+
# --- 89. шапка doctor читает манифест, а не литеральные пути ------------------
|
|
1328
|
+
# Отзыв второго пользователя 2026-09-08: у проекта `rules: .temper/rules`, правила на месте,
|
|
1329
|
+
# СТУПЕНЬ по манифесту берётся — а шапка рисовала кресты за `.aqk/rules` и `.aqk/docs` и
|
|
1330
|
+
# советовала сделать сделанное. Вывод расходился с собственным вердиктом программы, напечатанным
|
|
1331
|
+
# на десять строк ниже. Модульная проверка сторожит функцию; эта — то, что её кто-то зовёт.
|
|
1332
|
+
LYP="$(mktemp -d)"
|
|
1333
|
+
( cd "$LYP" && git init -q . && mkdir -p .temper/rules .temper/docs &&
|
|
1334
|
+
printf 'правило\n' > .temper/rules/r.md && printf 'методичка\n' > .temper/docs/d.md &&
|
|
1335
|
+
printf '# вход\n' > CLAUDE.md && printf '.x\n' > .gitignore &&
|
|
1336
|
+
printf 'aqk: 1\nentry:\n - CLAUDE.md\nrules: .temper/rules\ndocs: .temper/docs\ngates:\n smoke: "true"\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1337
|
+
LY=$( cd "$LYP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1338
|
+
if printf '%s' "$LY" | grep -q "\.temper/rules" &&
|
|
1339
|
+
printf '%s' "$LY" | grep -q "\.temper/docs" &&
|
|
1340
|
+
printf '%s' "$LY" | grep -q "CLAUDE\.md" &&
|
|
1341
|
+
! printf '%s' "$LY" | grep -q "\.aqk/rules" &&
|
|
1342
|
+
! printf '%s' "$LY" | grep -q "неизвестное поле"; then
|
|
1343
|
+
ok "шапка doctor берёт правила, методички и вход из манифеста"
|
|
1344
|
+
else
|
|
1345
|
+
bad "doctor проверил не то, что объявлено в манифесте" "$(printf '%s' "$LY" | head -8)"
|
|
1346
|
+
fi
|
|
1347
|
+
rm -rf "$LYP"
|
|
1348
|
+
|
|
1349
|
+
# --- 90. блок состояния: тишина не выдаётся за «чисто» -----------------------
|
|
1350
|
+
# Читатель этого блока — машина. Человек, увидев пустое место, переспросит; агент примет его
|
|
1351
|
+
# за утверждение и пойдёт писать код по несуществующему разрешению. Поэтому главное здесь одно:
|
|
1352
|
+
# без прогона блок обязан сказать «неизвестно» СЛОВОМ.
|
|
1353
|
+
CTXP="$(mktemp -d)"
|
|
1354
|
+
( cd "$CTXP" && git init -q . ) >/dev/null 2>&1
|
|
1355
|
+
CTX=$( cd "$CTXP" && AQK_LANG=ru node "$CLI" context 2>&1 ); CTX_C=$?
|
|
1356
|
+
if [ "$CTX_C" -eq 0 ] &&
|
|
1357
|
+
printf '%s' "$CTX" | grep -q "НЕИЗВЕСТНО" &&
|
|
1358
|
+
printf '%s' "$CTX" | grep -q "не вычислен" &&
|
|
1359
|
+
! printf '%s' "$CTX" | grep -q "AGENTS.md"; then
|
|
1360
|
+
ok "context без прогона говорит «неизвестно» и не называет несуществующий свод"
|
|
1361
|
+
else
|
|
1362
|
+
bad "context выдал незнание за чистоту" "код $CTX_C: $(printf '%s' "$CTX" | head -5)"
|
|
1363
|
+
fi
|
|
1364
|
+
|
|
1365
|
+
# --- 91. хук ставится в общий файл и не затирает чужие настройки --------------
|
|
1366
|
+
mkdir -p "$CTXP/.claude"
|
|
1367
|
+
printf '{ "permissions": { "deny": ["Read(./.env)"] } }\n' > "$CTXP/.claude/settings.json"
|
|
1368
|
+
( cd "$CTXP" && AQK_LANG=ru node "$CLI" context --install ) >/dev/null 2>&1
|
|
1369
|
+
AGAIN=$( cd "$CTXP" && AQK_LANG=ru node "$CLI" context --install 2>&1 )
|
|
1370
|
+
HOOKS=$(node_in "$CTXP" -e 'const s=require("./.claude/settings.json");
|
|
1371
|
+
console.log([s.hooks?.SessionStart?.length, s.permissions?.deny?.length,
|
|
1372
|
+
/[/\\]program\.mjs/.test(JSON.stringify(s.hooks?.SessionStart||[]))].join(" "))' 2>&1)
|
|
1373
|
+
if [ "$HOOKS" = "1 1 false" ] && printf '%s' "$AGAIN" | grep -q "уже стоит"; then
|
|
1374
|
+
ok "хук ставится один раз, переносимой командой, чужие настройки целы"
|
|
1375
|
+
else
|
|
1376
|
+
bad "установка хука испортила настройки или задвоилась" "разбор: $HOOKS"
|
|
1377
|
+
fi
|
|
1378
|
+
rm -rf "$CTXP"
|
|
1379
|
+
|
|
1380
|
+
# --- 92. полный блок: карта и свод дословно ----------------------------------
|
|
1381
|
+
# Решение владельца 2026-09-08, принятое ПОСЛЕ возражения про длину входа: агент читает файлы
|
|
1382
|
+
# плохо, и лишние токены — плата за то, чтобы он не ошибался. Раз плата внесена, товар обязан
|
|
1383
|
+
# быть доставлен: свод дословно, а не пересказ, и карта команд, а не половина карты.
|
|
1384
|
+
FULP="$(mktemp -d)"
|
|
1385
|
+
( cd "$FULP" && git init -q . &&
|
|
1386
|
+
printf '# Свод\n\n- Правило-маячок-для-проверки. <!-- aqk: человек -->\n' > AGENTS.md &&
|
|
1387
|
+
printf 'aqk: 1\nentry:\n - AGENTS.md\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1388
|
+
FUL=$( cd "$FULP" && AQK_LANG=ru node "$CLI" context --full 2>&1 )
|
|
1389
|
+
SHORT=$( cd "$FULP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1390
|
+
if printf '%s' "$FUL" | grep -q "Правило-маячок-для-проверки" &&
|
|
1391
|
+
printf '%s' "$FUL" | grep -q "doctor --run --since main" &&
|
|
1392
|
+
printf '%s' "$FUL" | grep -q "ЧТО УМЕЕТ ЭТОТ ИНСТРУМЕНТ" &&
|
|
1393
|
+
! printf '%s' "$SHORT" | grep -q "Правило-маячок-для-проверки"; then
|
|
1394
|
+
ok "context --full несёт карту и свод дословно, обычный — нет"
|
|
1395
|
+
else
|
|
1396
|
+
bad "полный блок не донёс свод или карту" "$(printf '%s' "$FUL" | head -4)"
|
|
1397
|
+
fi
|
|
1398
|
+
|
|
1399
|
+
# --- 93. хук с --full ставится именно с флагом -------------------------------
|
|
1400
|
+
( cd "$FULP" && AQK_LANG=ru node "$CLI" context --full --install ) >/dev/null 2>&1
|
|
1401
|
+
FLAG=$(node_in "$FULP" -e 'const s=require("./.claude/settings.json");
|
|
1402
|
+
console.log(JSON.stringify(s.hooks?.SessionStart||[]).includes("--full"))' 2>&1)
|
|
1403
|
+
if [ "$FLAG" = "true" ]; then
|
|
1404
|
+
ok "хук, поставленный с --full, зовёт полный блок, а не короткий"
|
|
1405
|
+
else
|
|
1406
|
+
bad "хук потерял --full — вливался бы короткий блок" "разбор: $FLAG"
|
|
1407
|
+
fi
|
|
1408
|
+
rm -rf "$FULP"
|
|
1409
|
+
|
|
1410
|
+
# --- 94. отсев сгенерированных файлов: края, а не середина -------------------
|
|
1411
|
+
# `drop_generated` заменил цикл `while read; do is_generated; done` — два процесса на файл.
|
|
1412
|
+
# Замер на шести чужих проектах дал ноль расхождений, но отсеяно там было 5 файлов из 1242:
|
|
1413
|
+
# такая сверка почти ничего не доказывает. Набор ниже нарочный, и он сразу нашёл ДВА
|
|
1414
|
+
# расхождения, которых на живых проектах не было, — пробел и двоеточие в имени файла.
|
|
1415
|
+
GENP="$(mktemp -d)"
|
|
1416
|
+
mkg() { printf '%b' "$2" > "$GENP/$1"; }
|
|
1417
|
+
mkg 'l1.js' '// @generated\ncode\n'
|
|
1418
|
+
mkg 'l5.js' 'a\nb\nc\nd\n// @generated\n'
|
|
1419
|
+
mkg 'l6.js' 'a\nb\nc\nd\ne\n// @generated\n'
|
|
1420
|
+
mkg 'upper.js' '// @GENERATED\n'
|
|
1421
|
+
mkg 'donotedit.js' '// Do Not Edit\n'
|
|
1422
|
+
mkg 'autogen.js' '// auto-generated by tool\n'
|
|
1423
|
+
mkg 'genby.js' '// Generated by protoc\n'
|
|
1424
|
+
mkg 'cyr.js' '// сгенерирован автоматически\n'
|
|
1425
|
+
mkg 'cyrup.js' '// СГЕНЕРИРОВАН\n'
|
|
1426
|
+
mkg 'with space.js' '// @generated\n'
|
|
1427
|
+
mkg 'with:colon.js' '// @generated\n'
|
|
1428
|
+
mkg 'clean.js' 'const x = 1;\n'
|
|
1429
|
+
: > "$GENP/empty.js"
|
|
1430
|
+
KEPT=$( . "$ROOT/kit/gates/_skip.sh"; find "$GENP" -type f | sort | drop_generated | sed "s#$GENP/##" | sort | tr '\n' ' ' )
|
|
1431
|
+
# Ожидания абсолютные, а не «как было раньше»: старая функция однажды уйдёт, а края останутся.
|
|
1432
|
+
# l6 остаётся намеренно: маркер на шестой строке — это уже не шапка файла.
|
|
1433
|
+
if [ "$KEPT" = "clean.js empty.js l6.js " ]; then
|
|
1434
|
+
ok "сгенерированные отсеяны по всем маркерам; пробел, двоеточие и граница 5-й строки учтены"
|
|
1435
|
+
else
|
|
1436
|
+
bad "отсев сгенерированных изменился" "осталось: $KEPT"
|
|
1437
|
+
fi
|
|
1438
|
+
rm -rf "$GENP"
|
|
1439
|
+
|
|
1200
1440
|
# --- итог -------------------------------------------------------------------
|
|
1201
1441
|
printf '\n'
|
|
1202
1442
|
if [ "$FAIL" -eq 0 ]; then
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// tool/selfcheck/units-context.mjs — блок состояния, который уходит В КОНТЕКСТ агента.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНЫМ ФАЙЛОМ. У этого текста единственный читатель — машина, и цена ошибки другая,
|
|
4
|
+
// чем у терминального вывода: человек, увидев пустую строку, переспросит, а агент примет её за
|
|
5
|
+
// утверждение. Поэтому главная проверка здесь одна и та же во всех видах: **тишина не означает
|
|
6
|
+
// «чисто»**. Блок обязан говорить «неизвестно» там, где не знает, — иначе он врёт ровно тем
|
|
7
|
+
// способом, против которого написан весь комплект.
|
|
8
|
+
//
|
|
9
|
+
// node --test tool/selfcheck/units-context.mjs
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { contextBlock, countArbiters, parseLastRun, withHook, hasOurHook, portableSelf } from "../commands/context.mjs";
|
|
13
|
+
import { CATALOGS } from "../i18n/index.mjs";
|
|
14
|
+
import { commandRows } from "../lib/core.mjs";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
|
|
17
|
+
const T = CATALOGS.ru.context;
|
|
18
|
+
const base = {
|
|
19
|
+
entry: "AGENTS.md",
|
|
20
|
+
level: { reached: 1, top: 3, missing: "гейты не доказаны" },
|
|
21
|
+
rules: { total: 14, machine: 2, human: 12 },
|
|
22
|
+
run: { when: "2026-09-08 11:00", red: [], skipped: 0, stale: false },
|
|
23
|
+
ratchets: [],
|
|
24
|
+
};
|
|
25
|
+
const text = (over = {}) => contextBlock({ ...base, ...over }, T).join("\n");
|
|
26
|
+
|
|
27
|
+
// ГЛАВНАЯ. Прогона не было — сказать «неизвестно» словом. Пустая строка на этом месте
|
|
28
|
+
// прочитается агентом как «красных нет», и он пойдёт писать код по несуществующему разрешению.
|
|
29
|
+
test("без прогона блок говорит «неизвестно», а не молчит", () => {
|
|
30
|
+
const t = text({ run: null });
|
|
31
|
+
assert.match(t, /НЕИЗВЕСТНО/);
|
|
32
|
+
assert.doesNotMatch(t, /красных нет/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("красные гейты названы поимённо", () => {
|
|
36
|
+
const t = text({ run: { ...base.run, red: ["file-size-limit", "duplicate-code"] } });
|
|
37
|
+
assert.match(t, /file-size-limit/);
|
|
38
|
+
assert.match(t, /duplicate-code/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Проект с двадцатью красными не должен вытеснять собой весь контекст: ровно та деградация,
|
|
42
|
+
// ради избежания которой блок и делается коротким.
|
|
43
|
+
test("длинный список красных обрезается и называет остаток числом", () => {
|
|
44
|
+
const red = Array.from({ length: 20 }, (_, i) => `гейт-${i}`);
|
|
45
|
+
const t = text({ run: { ...base.run, red } });
|
|
46
|
+
assert.match(t, /ещё 15/);
|
|
47
|
+
assert.ok(!t.includes("гейт-9"), "шестой и дальше в список не попадают");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Прогон, сделанный до последнего коммита, описывает не тот код, что лежит перед агентом.
|
|
51
|
+
test("устаревший прогон помечен, а не выдан за свежий", () => {
|
|
52
|
+
const t = text({ run: { ...base.run, stale: true } });
|
|
53
|
+
assert.match(t, /СТАРЕЕ/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// То, ради чего второй пользователь и оценил promise-has-gate: «держит человек» значит
|
|
57
|
+
// «не держит никто», и это обязано быть сказано словами, а не выведено читателем из цифр.
|
|
58
|
+
test("правила без машинного арбитра названы прямо", () => {
|
|
59
|
+
const t = text({ rules: { total: 12, machine: 0, human: 12 } });
|
|
60
|
+
assert.match(t, /не держит никто/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("нет манифеста — уровень не выдумывается", () => {
|
|
64
|
+
const t = text({ level: null });
|
|
65
|
+
assert.match(t, /не вычислен/);
|
|
66
|
+
assert.doesNotMatch(t, /AQK-0 из/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Потолок: блок влезает в глаза целиком. Замер 2026-09-08 — вход целиком в контекст роняет
|
|
70
|
+
// точность у всех проверенных моделей по мере роста, поэтому предел здесь предмет проверки,
|
|
71
|
+
// а не пожелание.
|
|
72
|
+
test("блок не разрастается даже на худшем входе", () => {
|
|
73
|
+
const lines = contextBlock({
|
|
74
|
+
...base,
|
|
75
|
+
run: { when: "…", red: Array.from({ length: 40 }, (_, i) => `г-${i}`), skipped: 9, stale: true },
|
|
76
|
+
ratchets: Array.from({ length: 12 }, (_, i) => ({ name: `р-${i}`, count: i })),
|
|
77
|
+
}, T);
|
|
78
|
+
assert.ok(lines.length <= 16, `строк ${lines.length}, предел 16`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// --- разбор источников -------------------------------------------------------
|
|
82
|
+
// Имя арбитра — это имя гейта, а в именах гейтов есть дефисы. Класс `[^\s>-]` обрывал их
|
|
83
|
+
// молча: первый же живой запуск показал 13 правил вместо 14 и одного машинного вместо двух.
|
|
84
|
+
// Молча — потому что число выглядит правдоподобным, пока его не с чем сверить.
|
|
85
|
+
test("имя арбитра с дефисом считается машинным, а не теряется", () => {
|
|
86
|
+
const md = [
|
|
87
|
+
"- Правило один. <!-- aqk: deps-are-pinned -->",
|
|
88
|
+
"- Правило два. <!-- aqk: человек -->",
|
|
89
|
+
"- Правило три. <!-- aqk: gates -->",
|
|
90
|
+
].join("\n");
|
|
91
|
+
assert.deepEqual(countArbiters(md, ["человек"]), { total: 3, machine: 2, human: 1 });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("отчёт прошлого прогона отдаёт красные и число непроверенных", () => {
|
|
95
|
+
const r = parseLastRun([
|
|
96
|
+
"# aqk doctor --run — 2026-09-08 11:00",
|
|
97
|
+
"level: AQK-1",
|
|
98
|
+
"✔ smoke — 1.0s",
|
|
99
|
+
"✘ file-size-limit — 0.1s",
|
|
100
|
+
"~ dead-code — нет инструмента",
|
|
101
|
+
].join("\n"));
|
|
102
|
+
assert.equal(r.when, "2026-09-08 11:00");
|
|
103
|
+
assert.deepEqual(r.red, ["file-size-limit"]);
|
|
104
|
+
assert.equal(r.skipped, 1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("пустой отчёт — это не «чисто», а отсутствие данных", () => {
|
|
108
|
+
assert.equal(parseLastRun(""), null);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Найдено ЗАМЕРОМ на шести чужих проектах, до того как хук попал в init: на `flask`, где нет
|
|
112
|
+
// ни .aqk.yml, ни AGENTS.md, блок всё равно писал «Свод правил: AGENTS.md». Тот же класс, что
|
|
113
|
+
// у doctor неделей раньше: умолчание выдаётся за факт, потому что на нашем репозитории
|
|
114
|
+
// умолчание и факт совпадают. Назвать агенту несуществующий файл хуже, чем промолчать: он
|
|
115
|
+
// пойдёт его читать и получит пустоту вместо правил.
|
|
116
|
+
test("несуществующая точка входа не называется как свод правил", () => {
|
|
117
|
+
const t = text({ level: null, run: null, rules: null, entryExists: false });
|
|
118
|
+
assert.doesNotMatch(t, /AGENTS\.md/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("существующая точка входа называется", () => {
|
|
122
|
+
const t = text({ entryExists: true });
|
|
123
|
+
assert.match(t, /AGENTS\.md/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- хук уходит в ОБЩИЙ файл настроек ----------------------------------------
|
|
127
|
+
// `.claude/settings.json` кладут в git: он общий на команду, в отличие от settings.local.json.
|
|
128
|
+
// Значит команда внутри него обязана работать не только на той машине, где её записали.
|
|
129
|
+
// На машине разработчика SELF — абсолютный путь; у соседа такого пути нет, и хук молча
|
|
130
|
+
// не сработает. Молча — то есть блок состояния просто не появится, и никто не узнает.
|
|
131
|
+
test("абсолютный путь заменяется переносимым вызовом", () => {
|
|
132
|
+
assert.equal(portableSelf("node /home/x/aqk/tool/program.mjs"), "npx agent-quality-kit");
|
|
133
|
+
assert.equal(portableSelf("node C:\\x\\aqk\\tool\\program.mjs"), "npx agent-quality-kit");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("переносимые вызовы остаются как есть", () => {
|
|
137
|
+
assert.equal(portableSelf("aqk"), "aqk");
|
|
138
|
+
assert.equal(portableSelf("npx agent-quality-kit"), "npx agent-quality-kit");
|
|
139
|
+
assert.equal(portableSelf("node tool/program.mjs"), "node tool/program.mjs");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Второй такой же хук — блок в контексте дважды: вдвое больше токенов и ровно ноль пользы.
|
|
143
|
+
test("хук не задваивается", () => {
|
|
144
|
+
const once = withHook({}, "aqk context");
|
|
145
|
+
assert.ok(hasOurHook(once, "aqk context"));
|
|
146
|
+
assert.equal(once.hooks.SessionStart.length, 1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Чужие настройки в том же файле — права доступа, другие хуки — обязаны пережить установку.
|
|
150
|
+
test("чужие настройки переживают установку хука", () => {
|
|
151
|
+
const before = { permissions: { deny: ["Read(./.env)"] }, hooks: { Stop: [{ hooks: [] }] } };
|
|
152
|
+
const after = withHook(before, "aqk context");
|
|
153
|
+
assert.deepEqual(after.permissions, before.permissions);
|
|
154
|
+
assert.equal(after.hooks.Stop.length, 1);
|
|
155
|
+
assert.equal(after.hooks.SessionStart.length, 1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- карта команд ------------------------------------------------------------
|
|
159
|
+
// Владелец: «агент плохо читает инструкцию — нужно влить карту и сам свод». Карта имеет смысл
|
|
160
|
+
// ровно до тех пор, пока она не отстала от программы. Второй список, живущий рядом с первым,
|
|
161
|
+
// через месяц врёт — это записано у нас в README про храповики и верно здесь буквально так же.
|
|
162
|
+
// Поэтому карта и справка собираются ИЗ ОДНОГО списка, а эта проверка сторожит, что список
|
|
163
|
+
// не отстал от диспетчера: команда, добавленная в switch и забытая в списке, роняет её.
|
|
164
|
+
test("карта команд не отстаёт от диспетчера", async () => {
|
|
165
|
+
const src = await readFile(new URL("../program.mjs", import.meta.url), "utf8");
|
|
166
|
+
const dispatched = [...src.matchAll(/^\s{4}case "([a-z-]+)":/gm)].map((m) => m[1]);
|
|
167
|
+
assert.ok(dispatched.length >= 10, `в диспетчере найдено ${dispatched.length} команд — разбор сломался`);
|
|
168
|
+
const listed = new Set(commandRows(CATALOGS.ru).map((r) => r.name));
|
|
169
|
+
const missing = dispatched.filter((n) => !listed.has(n));
|
|
170
|
+
assert.deepEqual(missing, [], `в карте нет: ${missing.join(", ")}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Полный блок — то, за что владелец согласился платить токенами: свод правил дословно, а не
|
|
174
|
+
// ссылка на него. Если он не дословный, плата внесена, а товар не получен.
|
|
175
|
+
test("полный блок несёт свод правил дословно", () => {
|
|
176
|
+
const rules = "- Правило одно. <!-- aqk: человек -->\n- Правило два.";
|
|
177
|
+
const t = contextBlock({ ...base, entryExists: true, full: { entry: "AGENTS.md", rows: [{ cmd: "aqk context", text: "состояние" }], text: rules } }, T).join("\n");
|
|
178
|
+
assert.ok(t.includes(rules), "текст свода обязан войти целиком");
|
|
179
|
+
assert.match(t, /aqk context/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("без --full свод не вливается — только ссылка на него", () => {
|
|
183
|
+
const t = text({ entryExists: true });
|
|
184
|
+
assert.doesNotMatch(t, /Правило одно/);
|
|
185
|
+
assert.match(t, /AGENTS\.md/);
|
|
186
|
+
});
|