agent-quality-kit 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -12
- package/README.ru.md +185 -27
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +188 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/color-from-token/check.sh +5 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +92 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +25 -4
- package/package.json +3 -6
- package/tool/commands/context.mjs +37 -6
- package/tool/commands/doctor.mjs +139 -16
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/project.mjs +18 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/vitals.mjs +167 -0
- package/tool/i18n/en-docs.mjs +48 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +26 -279
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +48 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +26 -278
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +3 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +146 -15
- package/tool/lib/prove.mjs +11 -1
- package/tool/lib/repo.mjs +43 -3
- package/tool/program.mjs +33 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +528 -6
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +3 -1
- package/tool/selfcheck/units-level.mjs +147 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +164 -0
- package/tool/selfcheck/units-vitals.mjs +81 -0
- package/tool/selfcheck/units.mjs +3 -75
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tool/selfcheck/units-repo.mjs — осмотр репозитория: язык по расширению, триггеры записей,
|
|
2
|
+
// выбор рецепта, поиск программы в PATH, совет про браузер у агента.
|
|
3
|
+
//
|
|
4
|
+
// ОТДЕЛЬНЫМ ФАЙЛОМ, а не в units.mjs: тот снова перерос собственный предел в 500 строк, и поймал
|
|
5
|
+
// это наш же file-size-limit. Шов по смыслу: здесь всё, что программа УЗНАЁТ О ЧУЖОМ РЕПОЗИТОРИИ
|
|
6
|
+
// и что из этого следует, — а в units.mjs осталось то, что она делает со своими данными.
|
|
7
|
+
//
|
|
8
|
+
// node --test tool/selfcheck/units-repo.mjs
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec } from "../lib/repo.mjs";
|
|
12
|
+
import { CATALOGS, L } from "../i18n/index.mjs";
|
|
13
|
+
import { dirname } from "node:path";
|
|
14
|
+
|
|
15
|
+
const facts = (over = {}) => ({ langs: new Set(), files: 0, ...over });
|
|
16
|
+
|
|
17
|
+
// --- опознание языка по расширению -------------------------------------------
|
|
18
|
+
// Найдено на самом aqk: вся программа лежит в .mjs, и запись про отладочную печать
|
|
19
|
+
// пряталась с пояснением «нет языков: javascript» — в проекте, целиком на JavaScript.
|
|
20
|
+
test("расширение .mjs — это JavaScript", () => {
|
|
21
|
+
assert.equal(EXT_LANG[".mjs"], "javascript");
|
|
22
|
+
assert.equal(EXT_LANG[".cjs"], "javascript");
|
|
23
|
+
assert.equal(EXT_LANG[".mts"], "typescript");
|
|
24
|
+
assert.equal(EXT_LANG[".py"], "python");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// --- триггер ------------------------------------------------------------------
|
|
29
|
+
test("без триггера запись не показывается", () => {
|
|
30
|
+
assert.equal(triggerVerdict({}, facts()).applies, false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("условия складываются по И: одно ложное скрывает запись", () => {
|
|
34
|
+
const rec = { trigger: { langs: "python", files_gt: "10" } };
|
|
35
|
+
assert.equal(triggerVerdict(rec, facts({ langs: new Set(["python"]), files: 50 })).applies, true);
|
|
36
|
+
assert.equal(triggerVerdict(rec, facts({ langs: new Set(["python"]), files: 3 })).applies, false);
|
|
37
|
+
assert.equal(triggerVerdict(rec, facts({ langs: new Set(["go"]), files: 50 })).applies, false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("причина, по которой запись скрыта, называется словами", () => {
|
|
41
|
+
const v = triggerVerdict({ trigger: { langs: "python, typescript" } }, facts({ langs: new Set(["go"]) }));
|
|
42
|
+
// Сверяем с каталогом, а не с буквами: текст переводится, а выбор причины — нет.
|
|
43
|
+
assert.equal(v.why, L.trigger.noLangs("python, typescript"));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("always: false значит «никогда не применимо», а не «условие пропущено»", () => {
|
|
47
|
+
const v = triggerVerdict({ trigger: { always: "false" } }, facts());
|
|
48
|
+
assert.equal(v.applies, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("неизвестное условие скрывает запись, а не пропускает её", () => {
|
|
52
|
+
// Молча пропустить незнакомое условие значит показать запись всем подряд.
|
|
53
|
+
const v = triggerVerdict({ trigger: { has_kubernetes: "true" } }, facts());
|
|
54
|
+
assert.equal(v.applies, false);
|
|
55
|
+
assert.equal(v.why, L.trigger.unknown("has_kubernetes"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- выбор рецепта ------------------------------------------------------------
|
|
59
|
+
test("без родного языка берётся переносимый рецепт, {dir} подставляется", () => {
|
|
60
|
+
const cmd = recipeFor({ slug: "x", recipes: { any: "bash {gate}/check.sh {dir}" } }, facts());
|
|
61
|
+
assert.match(cmd, /check\.sh \.$/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("рецепта нет — так и сказано, а не пустая строка", () => {
|
|
65
|
+
assert.equal(recipeFor({ slug: "x", recipes: {} }, facts()), L.recipe.none);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// --- поиск программы в PATH ---------------------------------------------------
|
|
70
|
+
// ЗАЧЕМ. Раньше наличие программы проверялось через `command -v` в оболочке. На Windows
|
|
71
|
+
// оболочка — cmd.exe, где такой команды нет, и ответ был «не установлено» ДЛЯ ЛЮБОЙ
|
|
72
|
+
// программы: родной рецепт становился недостижим, гейт молча вставал на слабейший
|
|
73
|
+
// переносимый вариант, а прогон показывал зелёное. Нашлось на чужом прогоне, не у нас.
|
|
74
|
+
test("программа в PATH находится, несуществующая — нет", () => {
|
|
75
|
+
assert.ok(whichSync("node"), "node обязан находиться: им же запущена эта проверка");
|
|
76
|
+
assert.equal(whichSync("нет-такой-программы-12345"), null);
|
|
77
|
+
assert.equal(whichSync(""), null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("поиск не зависит от оболочки — работает с пустым окружением", () => {
|
|
81
|
+
// Тот самый случай: оболочки нет или она другая. Ответ обязан быть «не нашли»,
|
|
82
|
+
// а не исключение и не ложное «нашли».
|
|
83
|
+
assert.equal(whichSync("node", { PATH: "" }), null);
|
|
84
|
+
const dir = dirname(process.execPath);
|
|
85
|
+
assert.ok(whichSync(process.platform === "win32" ? "node" : "node", { PATH: dir }));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("команда путём, а не именем, ищется на диске, а не в PATH", () => {
|
|
89
|
+
assert.ok(whichSync(process.execPath));
|
|
90
|
+
assert.equal(whichSync("./нет-такого-файла.sh"), null);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
// --- совет про браузерный MCP-сервер ------------------------------------------
|
|
95
|
+
// Решение владельца 2026-09-08: инструмент, дающий агенту браузер, надо РЕКОМЕНДОВАТЬ.
|
|
96
|
+
// Возражение про нейтральность к вендору здесь не работает: MCP — межвендорный протокол,
|
|
97
|
+
// и сервер одинаково нужен Cursor, Codex и Claude Code.
|
|
98
|
+
//
|
|
99
|
+
// Но совет показывается не всем. Проекту без интерфейса браузер не нужен, а совет, показанный
|
|
100
|
+
// не тому, стоит доверия всем остальным советам — та же норма, что у записей каталога.
|
|
101
|
+
test("совет про браузер даётся проекту с интерфейсом, у которого сервера нет", () => {
|
|
102
|
+
assert.ok(browserServerAdvice({ has_ui: true }, ""));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("проекту без интерфейса совет не даётся", () => {
|
|
106
|
+
assert.equal(browserServerAdvice({ has_ui: false }, ""), null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Уже поставил — молчим. Совет, повторяемый тому, кто его выполнил, читается как шум,
|
|
110
|
+
// и следующий совет он пролистает вместе с этим.
|
|
111
|
+
test("сервер уже объявлен — совета нет", () => {
|
|
112
|
+
const cfg = '{"mcpServers":{"browser":{"command":"npx","args":["-y","chrome-devtools-mcp@1.9.0"]}}}';
|
|
113
|
+
assert.equal(browserServerAdvice({ has_ui: true }, cfg), null);
|
|
114
|
+
const pw = '{"mcpServers":{"b":{"command":"npx","args":["@playwright/mcp@0.0.80"]}}}';
|
|
115
|
+
assert.equal(browserServerAdvice({ has_ui: true }, pw), null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Признак «здесь есть договор с чужим кодом»: спецификация API. Считается обходом дерева, а не
|
|
119
|
+
// списком путей, — файл кладут где угодно (`openapi.yaml`, `docs/api/openapi.json`, `schema.yml`
|
|
120
|
+
// рядом с приложением), и фиксированный список промахнулся бы на большинстве проектов.
|
|
121
|
+
//
|
|
122
|
+
// ПОЧЕМУ ПРИЗНАК ОТДЕЛЬНЫЙ, а не часть has_deps. Запись про арбитра контракта не касается
|
|
123
|
+
// библиотеки, консольной программы и монолита без внешнего интерфейса — а таких большинство.
|
|
124
|
+
// Показанной не тому записи не верят, и каталог теряет доверие целиком, а не одной строкой.
|
|
125
|
+
test("спецификация API опознаётся по имени файла, а не по расположению", () => {
|
|
126
|
+
assert.equal(isApiSpec("openapi.yaml"), true);
|
|
127
|
+
assert.equal(isApiSpec("openapi.json"), true);
|
|
128
|
+
assert.equal(isApiSpec("swagger.yml"), true);
|
|
129
|
+
assert.equal(isApiSpec("asyncapi.yaml"), true);
|
|
130
|
+
assert.equal(isApiSpec("openapi-v2.yaml"), true);
|
|
131
|
+
// Имя без расширения спецификации — не она: договор лежит в разбираемом формате.
|
|
132
|
+
assert.equal(isApiSpec("openapi.md"), false);
|
|
133
|
+
// Соседи по алфавиту, которые не договор: обычные файлы проекта.
|
|
134
|
+
assert.equal(isApiSpec("api.yaml"), false);
|
|
135
|
+
assert.equal(isApiSpec("schema.sql"), false);
|
|
136
|
+
assert.equal(isApiSpec("package.json"), false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Объяснение обязано быть на обоих языках. Признак без объяснения выключает запись НАВСЕГДА и
|
|
140
|
+
// молча — тот же класс, что поймал has_mcp ниже, поэтому сторожится отдельной строкой: в MARKS
|
|
141
|
+
// этого признака нет, он считается обходом, и общая проверка его не увидит.
|
|
142
|
+
test("у признака спецификации API есть объяснение на обоих языках", () => {
|
|
143
|
+
for (const lang of ["ru", "en"]) {
|
|
144
|
+
assert.ok(CATALOGS[lang].trigger.flags.has_api_spec, `${lang}: нет объяснения has_api_spec`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Признак репозитория без объяснения — это запись каталога, ВЫКЛЮЧЕННАЯ НАВСЕГДА. Триггер
|
|
149
|
+
// с неизвестным ключом даёт «условие программа не умеет считать», и запись не показывается
|
|
150
|
+
// никому и никогда. Поймано на себе 2026-09-08: завёл has_mcp в списке признаков, объяснение
|
|
151
|
+
// не завёл, и новая запись стала неприменимой в любом репозитории. Видно это было только в
|
|
152
|
+
// выводе doctor на чужой папке — ни один прогон не краснел.
|
|
153
|
+
test("у каждого признака репозитория есть объяснение на обоих языках", () => {
|
|
154
|
+
const names = MARKS.map(([n]) => n);
|
|
155
|
+
for (const lang of ["ru", "en"]) {
|
|
156
|
+
const flags = CATALOGS[lang].trigger.flags;
|
|
157
|
+
const missing = names.filter((n) => !flags[n]);
|
|
158
|
+
assert.deepEqual(missing, [], `${lang}: нет объяснения для ${missing.join(", ")}`);
|
|
159
|
+
// Обратной проверки нет намеренно, и это не лень. Часть признаков считается не по наличию
|
|
160
|
+
// файла, а обходом содержимого (has_db, has_tests, has_ui), и в этом списке их нет.
|
|
161
|
+
// Риск несимметричен: объяснение без признака — мёртвая строка, признак без объяснения —
|
|
162
|
+
// запись каталога, выключенная навсегда и молча. Сторожим ту сторону, которая ломает.
|
|
163
|
+
}
|
|
164
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// tool/selfcheck/units-vitals.mjs — «всё ли у самого комплекта подключено».
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ЭТА КОМАНДА. `doctor` смотрит на РЕПОЗИТОРИЙ, `prove` — на гейты, `context` — на
|
|
4
|
+
// состояние. На саму обвязку не смотрит никто: стоят ли инструменты, которых требуют
|
|
5
|
+
// объявленные гейты; прописан ли хук в `.git/hooks` на самом деле; получает ли агент состояние.
|
|
6
|
+
// Сегодня это выясняется красным гейтом посреди коммита — в худший момент из возможных.
|
|
7
|
+
//
|
|
8
|
+
// node --test tool/selfcheck/units-vitals.mjs
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { vitalsRows, vitalsVerdict } from "../commands/vitals.mjs";
|
|
12
|
+
|
|
13
|
+
const ok = { tools: [], unparsed: 0, preCommit: true, sessionHook: true, version: null };
|
|
14
|
+
|
|
15
|
+
// ТРИ СОСТОЯНИЯ, А НЕ ДВА. «Не подключено» и «не знаем» — разные ответы, и сливать их значит
|
|
16
|
+
// врать ровно тем способом, против которого написан весь комплект.
|
|
17
|
+
test("неизвестное не выдаётся за исправное", () => {
|
|
18
|
+
const rows = vitalsRows({ ...ok, preCommit: null });
|
|
19
|
+
const hook = rows.find((r) => r.key === "preCommit");
|
|
20
|
+
assert.equal(hook.ok, null, "не смогли посмотреть — значит неизвестно, а не «нет»");
|
|
21
|
+
// И «нет» — тоже отдельное состояние, не равное ни «да», ни «неизвестно».
|
|
22
|
+
assert.equal(vitalsRows({ ...ok, preCommit: false }).find((r) => r.key === "preCommit").ok, "no");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("отсутствующий инструмент объявленного гейта — отказ, а не мелочь", () => {
|
|
26
|
+
const rows = vitalsRows({ ...ok, tools: [{ gate: "lint", prog: "ruff", found: false }] });
|
|
27
|
+
const t = rows.find((r) => r.key === "tools");
|
|
28
|
+
assert.equal(t.ok, false);
|
|
29
|
+
assert.match(t.detail, /ruff/);
|
|
30
|
+
assert.match(t.detail, /lint/, "названо, КАКОЙ гейт останется без арбитра");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("все инструменты на месте — строка зелёная", () => {
|
|
34
|
+
const rows = vitalsRows({ ...ok, tools: [{ gate: "lint", prog: "ruff", found: true }] });
|
|
35
|
+
assert.equal(rows.find((r) => r.key === "tools").ok, true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("непонятые строки манифеста попадают в вердикт", () => {
|
|
39
|
+
const rows = vitalsRows({ ...ok, unparsed: 2 });
|
|
40
|
+
assert.equal(rows.find((r) => r.key === "manifest").ok, false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Код возврата: красное — отказ, неизвестное — не отказ. Иначе команда краснела бы у всех,
|
|
44
|
+
// у кого просто нет `.claude/`, и её выключили бы в первый день.
|
|
45
|
+
test("вердикт краснеет от отказов, но не от незнания и не от выбора", () => {
|
|
46
|
+
assert.equal(vitalsVerdict(vitalsRows(ok)), 0);
|
|
47
|
+
assert.equal(vitalsVerdict(vitalsRows({ ...ok, preCommit: null, sessionHook: null })), 0);
|
|
48
|
+
// Хука нет — это решение человека (гоняет в конвейере), а не поломка. Команда, которая
|
|
49
|
+
// кричит «сломано» про выбор, перестаёт читаться вместе с настоящими отказами.
|
|
50
|
+
assert.equal(vitalsVerdict(vitalsRows({ ...ok, preCommit: false, sessionHook: false })), 0);
|
|
51
|
+
assert.equal(vitalsVerdict(vitalsRows({ ...ok, unparsed: 1 })), 1);
|
|
52
|
+
assert.equal(vitalsVerdict(vitalsRows({ ...ok, tools: [{ gate: "g", prog: "x", found: false }] })), 1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// НАЙДЕНО АУДИТОМ ФИЧ 2026-09-09. Реестр ответил ошибкой (не упал запрос, а вернул не-200) —
|
|
56
|
+
// `latest` остаётся пустым, и строка печатала «0.9.0, свежая». То есть посмотреть не смогли, а
|
|
57
|
+
// сказали «всё хорошо»: ровно тот грех, против которого написан весь комплект, у него самого.
|
|
58
|
+
// Три состояния, а не два: свежая · старая · посмотреть не смогли.
|
|
59
|
+
test("не дозвонились до реестра — так и говорим, а не «свежая»", () => {
|
|
60
|
+
const unknown = vitalsRows({ ...ok, version: { current: "0.9.0", latest: "" } });
|
|
61
|
+
const u = unknown.find((r) => r.key === "version");
|
|
62
|
+
assert.equal(u.ok, null);
|
|
63
|
+
assert.match(u.detail, /0\.9\.0/);
|
|
64
|
+
// Утверждение по СМЫСЛУ, а не по слову: «свежая ли, неизвестно» тоже содержит корень «свеж».
|
|
65
|
+
// Первая версия этого теста ловила именно слово и краснела на честном тексте.
|
|
66
|
+
assert.match(u.detail, /неизвестн|unknown/i, `незнание не названо: ${u.detail}`);
|
|
67
|
+
|
|
68
|
+
const fresh = vitalsRows({ ...ok, version: { current: "0.9.0", latest: "0.9.0" } });
|
|
69
|
+
const fd = fresh.find((r) => r.key === "version").detail;
|
|
70
|
+
assert.doesNotMatch(fd, /неизвестн|unknown/i);
|
|
71
|
+
assert.notEqual(fd, u.detail, "«свежая» и «не знаем» неразличимы по выводу");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Устаревшая версия — не отказ: человек мог закрепить её сознательно.
|
|
75
|
+
test("старая версия сообщается, но не роняет", () => {
|
|
76
|
+
const rows = vitalsRows({ ...ok, version: { current: "0.8.0", latest: "0.9.0" } });
|
|
77
|
+
const v = rows.find((r) => r.key === "version");
|
|
78
|
+
assert.equal(v.ok, null);
|
|
79
|
+
assert.match(v.detail, /0\.9\.0/);
|
|
80
|
+
assert.equal(vitalsVerdict(rows), 0);
|
|
81
|
+
});
|
package/tool/selfcheck/units.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
14
|
import { parseManifest, manifestWithGate, unknownKeys, entryLifecycle, advisorySet, KNOWN_KEYS } from "../lib/manifest.mjs";
|
|
15
|
-
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync } from "../lib/repo.mjs";
|
|
15
|
+
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync, browserServerAdvice, MARKS } from "../lib/repo.mjs";
|
|
16
16
|
import { scopeOutput, splitAdvice } from "../lib/scope.mjs";
|
|
17
17
|
import { assessBaseline, ITEMS, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
18
18
|
import { CATALOGS, pickLang, L } from "../i18n/index.mjs";
|
|
@@ -21,16 +21,6 @@ import { dirname } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
const facts = (over = {}) => ({ langs: new Set(), files: 0, ...over });
|
|
23
23
|
|
|
24
|
-
// --- опознание языка по расширению -------------------------------------------
|
|
25
|
-
// Найдено на самом aqk: вся программа лежит в .mjs, и запись про отладочную печать
|
|
26
|
-
// пряталась с пояснением «нет языков: javascript» — в проекте, целиком на JavaScript.
|
|
27
|
-
test("расширение .mjs — это JavaScript", () => {
|
|
28
|
-
assert.equal(EXT_LANG[".mjs"], "javascript");
|
|
29
|
-
assert.equal(EXT_LANG[".cjs"], "javascript");
|
|
30
|
-
assert.equal(EXT_LANG[".mts"], "typescript");
|
|
31
|
-
assert.equal(EXT_LANG[".py"], "python");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
24
|
// --- разбор манифеста ---------------------------------------------------------
|
|
35
25
|
test("список читается и строкой в скобках, и пунктами", () => {
|
|
36
26
|
assert.deepEqual(parseManifest("entry: [AGENTS.md, CLAUDE.md]").entry, ["AGENTS.md", "CLAUDE.md"]);
|
|
@@ -43,46 +33,6 @@ test("вложенный блок читается словарём, комме
|
|
|
43
33
|
assert.equal(m.rules, "kit/rules");
|
|
44
34
|
});
|
|
45
35
|
|
|
46
|
-
// --- триггер ------------------------------------------------------------------
|
|
47
|
-
test("без триггера запись не показывается", () => {
|
|
48
|
-
assert.equal(triggerVerdict({}, facts()).applies, false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("условия складываются по И: одно ложное скрывает запись", () => {
|
|
52
|
-
const rec = { trigger: { langs: "python", files_gt: "10" } };
|
|
53
|
-
assert.equal(triggerVerdict(rec, facts({ langs: new Set(["python"]), files: 50 })).applies, true);
|
|
54
|
-
assert.equal(triggerVerdict(rec, facts({ langs: new Set(["python"]), files: 3 })).applies, false);
|
|
55
|
-
assert.equal(triggerVerdict(rec, facts({ langs: new Set(["go"]), files: 50 })).applies, false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("причина, по которой запись скрыта, называется словами", () => {
|
|
59
|
-
const v = triggerVerdict({ trigger: { langs: "python, typescript" } }, facts({ langs: new Set(["go"]) }));
|
|
60
|
-
// Сверяем с каталогом, а не с буквами: текст переводится, а выбор причины — нет.
|
|
61
|
-
assert.equal(v.why, L.trigger.noLangs("python, typescript"));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("always: false значит «никогда не применимо», а не «условие пропущено»", () => {
|
|
65
|
-
const v = triggerVerdict({ trigger: { always: "false" } }, facts());
|
|
66
|
-
assert.equal(v.applies, false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("неизвестное условие скрывает запись, а не пропускает её", () => {
|
|
70
|
-
// Молча пропустить незнакомое условие значит показать запись всем подряд.
|
|
71
|
-
const v = triggerVerdict({ trigger: { has_kubernetes: "true" } }, facts());
|
|
72
|
-
assert.equal(v.applies, false);
|
|
73
|
-
assert.equal(v.why, L.trigger.unknown("has_kubernetes"));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// --- выбор рецепта ------------------------------------------------------------
|
|
77
|
-
test("без родного языка берётся переносимый рецепт, {dir} подставляется", () => {
|
|
78
|
-
const cmd = recipeFor({ slug: "x", recipes: { any: "bash {gate}/check.sh {dir}" } }, facts());
|
|
79
|
-
assert.match(cmd, /check\.sh \.$/);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("рецепта нет — так и сказано, а не пустая строка", () => {
|
|
83
|
-
assert.equal(recipeFor({ slug: "x", recipes: {} }, facts()), L.recipe.none);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
36
|
// --- дедупликация по намерению ------------------------------------------------
|
|
87
37
|
test("разные намерения не путаются служебными словами", () => {
|
|
88
38
|
// Найдено на `aqk new dead-code-not-shipped`: слова not/in/code давали ложное совпадение
|
|
@@ -158,30 +108,6 @@ test("язык берётся из AQK_LANG, потом из локали, ин
|
|
|
158
108
|
assert.equal(pickLang({}), "en");
|
|
159
109
|
});
|
|
160
110
|
|
|
161
|
-
// --- поиск программы в PATH ---------------------------------------------------
|
|
162
|
-
// ЗАЧЕМ. Раньше наличие программы проверялось через `command -v` в оболочке. На Windows
|
|
163
|
-
// оболочка — cmd.exe, где такой команды нет, и ответ был «не установлено» ДЛЯ ЛЮБОЙ
|
|
164
|
-
// программы: родной рецепт становился недостижим, гейт молча вставал на слабейший
|
|
165
|
-
// переносимый вариант, а прогон показывал зелёное. Нашлось на чужом прогоне, не у нас.
|
|
166
|
-
test("программа в PATH находится, несуществующая — нет", () => {
|
|
167
|
-
assert.ok(whichSync("node"), "node обязан находиться: им же запущена эта проверка");
|
|
168
|
-
assert.equal(whichSync("нет-такой-программы-12345"), null);
|
|
169
|
-
assert.equal(whichSync(""), null);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("поиск не зависит от оболочки — работает с пустым окружением", () => {
|
|
173
|
-
// Тот самый случай: оболочки нет или она другая. Ответ обязан быть «не нашли»,
|
|
174
|
-
// а не исключение и не ложное «нашли».
|
|
175
|
-
assert.equal(whichSync("node", { PATH: "" }), null);
|
|
176
|
-
const dir = dirname(process.execPath);
|
|
177
|
-
assert.ok(whichSync(process.platform === "win32" ? "node" : "node", { PATH: dir }));
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("команда путём, а не именем, ищется на диске, а не в PATH", () => {
|
|
181
|
-
assert.ok(whichSync(process.execPath));
|
|
182
|
-
assert.equal(whichSync("./нет-такого-файла.sh"), null);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
111
|
// --- значок уровня ------------------------------------------------------------
|
|
186
112
|
// ЗАЧЕМ. Значок печатает одна функция, а читает его обратно другое выражение — в том же
|
|
187
113
|
// файле, но независимо. Разойдись они, и `badge --check` перестал бы узнавать собственный
|
|
@@ -470,3 +396,5 @@ test("наборы файлов правил совпадают на обоих
|
|
|
470
396
|
assert.equal(ru.length > 0, true);
|
|
471
397
|
});
|
|
472
398
|
|
|
399
|
+
|
|
400
|
+
|