agent-quality-kit 0.11.0 → 0.13.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 +28 -4
- package/README.ru.md +28 -4
- package/kit/gates/complexity-limit/gate.yml +9 -0
- package/kit/gates/dead-code/gate.yml +9 -0
- package/kit/gates/duplicate-code/gate.yml +5 -0
- package/kit/gates/env-secrets-not-committed/README.md +73 -0
- package/kit/gates/env-secrets-not-committed/check.sh +139 -0
- package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
- package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
- package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
- package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
- package/kit/gates/env-secrets-not-committed/red/.env +7 -0
- package/kit/gates/no-print-in-prod/gate.yml +9 -0
- package/kit/gates/secrets-not-in-code/gate.yml +20 -0
- package/kit/gates/swallowed-error/gate.yml +9 -0
- package/kit/gates/todo-without-task/gate.yml +9 -0
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +59 -4
- package/tool/commands/doctor.mjs +171 -44
- package/tool/commands/gates.mjs +3 -2
- package/tool/commands/probe.mjs +86 -22
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +21 -12
- package/tool/i18n/en-docs.mjs +24 -2
- package/tool/i18n/en-gates.mjs +10 -1
- package/tool/i18n/en.mjs +25 -0
- package/tool/i18n/ru-docs.mjs +26 -2
- package/tool/i18n/ru-gates.mjs +10 -1
- package/tool/i18n/ru.mjs +25 -0
- package/tool/lib/adopt.mjs +112 -0
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +46 -1
- package/tool/lib/core.mjs +39 -1
- package/tool/lib/execution.mjs +50 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +45 -6
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/prove.mjs +2 -2
- package/tool/lib/repo.mjs +18 -0
- package/tool/lib/run.mjs +108 -8
- package/tool/selfcheck/gates.sh +12 -2
- package/tool/selfcheck/smoke/first-run.test.mjs +105 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +51 -2
- package/tool/selfcheck/smoke.sh +6 -2
- package/tool/selfcheck/units-cadence.mjs +51 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-execution.mjs +78 -1
- package/tool/selfcheck/units-level.mjs +99 -1
- package/tool/selfcheck/units-probe.mjs +104 -0
- package/tool/selfcheck/units-repo.mjs +146 -0
- package/tool/selfcheck/units-verdict.mjs +76 -0
- package/tool/selfcheck/units.mjs +34 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// рядом с инструментом — нормализующим адаптером, а протокол остаётся простым.
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
|
-
import { classify, findingCodes } from "../lib/execution.mjs";
|
|
20
|
+
import { classify, findingCodes, gitBash, launchable, gateCommand } from "../lib/execution.mjs";
|
|
21
21
|
|
|
22
22
|
// Вход — то, что отдаёт spawnSync: { status, signal, error }.
|
|
23
23
|
const R = (over = {}) => ({ status: 0, signal: null, error: undefined, ...over });
|
|
@@ -86,3 +86,80 @@ test("адаптеры знают свои коды: vulture 3 — находк
|
|
|
86
86
|
test("наши собственные обёртки: 2 — «нет инструмента», а не находка", () => {
|
|
87
87
|
assert.equal(classify(R({ status: 2 }), findingCodes("bash")).state, "infra_error");
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
// --- какой bash запускать на Windows ---------------------------------------------------
|
|
91
|
+
// ЗАЧЕМ. На Windows 10/11 в System32 лежит bash.exe — заглушка WSL, и в PATH она стоит раньше
|
|
92
|
+
// Git Bash (установщик Git по умолчанию кладёт в PATH только Git\cmd). Команда гейта
|
|
93
|
+
// `bash kit/gates/x/check.sh .` уезжала в Linux-подсистему: другие программы, другой git,
|
|
94
|
+
// другие пути — и прогон краснел «не из-за кода». Отчёт с живого проекта 2026-09-11: «гейты
|
|
95
|
+
// под Windows берут bash из WSL. Через Git Bash все проходят». Известная ловушка, на неё
|
|
96
|
+
// наступали Claude Code (issue #23556) и Archon (#1326); общий приём — брать bash рядом с git.
|
|
97
|
+
const winFs = (files) => (p) => files.includes(p);
|
|
98
|
+
test("Windows: bash берётся рядом с git.exe, а не из PATH", () => {
|
|
99
|
+
const git = "C:\\Program Files\\Git\\cmd\\git.exe";
|
|
100
|
+
const bash = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
101
|
+
const got = gitBash({ platform: "win32", env: {}, which: () => git, exists: winFs([bash]) });
|
|
102
|
+
assert.equal(got, bash);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("Windows: git из mingw64 и запасной путь в Program Files тоже находятся", () => {
|
|
106
|
+
const bash = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
107
|
+
const deep = gitBash({ platform: "win32", env: {},
|
|
108
|
+
which: () => "C:\\Program Files\\Git\\mingw64\\bin\\git.exe", exists: winFs([bash]) });
|
|
109
|
+
assert.equal(deep, bash);
|
|
110
|
+
const pf = gitBash({ platform: "win32", env: { ProgramFiles: "C:\\Program Files" },
|
|
111
|
+
which: () => null, exists: winFs([bash]) });
|
|
112
|
+
assert.equal(pf, bash);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("Windows: явный AQK_BASH главнее поиска; не нашли — null, а не заглушка WSL", () => {
|
|
116
|
+
assert.equal(gitBash({ platform: "win32", env: { AQK_BASH: "D:\\x\\bash.exe" }, which: () => null, exists: () => false }),
|
|
117
|
+
"D:\\x\\bash.exe");
|
|
118
|
+
assert.equal(gitBash({ platform: "win32", env: {}, which: () => null, exists: () => false }), null);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("не Windows — bash из PATH как был, команда не трогается", () => {
|
|
122
|
+
assert.equal(gitBash({ platform: "linux", env: {}, which: () => "/usr/bin/git", exists: () => true }), null);
|
|
123
|
+
assert.equal(launchable("bash x.sh .", null), "bash x.sh .");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("подменяется только первое слово bash, в кавычках — путь с пробелом", () => {
|
|
127
|
+
const b = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
128
|
+
assert.equal(launchable("bash kit/gates/x/check.sh .", b), `"${b}" kit/gates/x/check.sh .`);
|
|
129
|
+
// Внутренний bash обёртки храповика запускает уже Git Bash — у него свой PATH, трогать незачем.
|
|
130
|
+
assert.equal(launchable("bash r/_ratchet.sh r/x.txt bash g/check.sh .", b), `"${b}" r/_ratchet.sh r/x.txt bash g/check.sh .`);
|
|
131
|
+
assert.equal(launchable("bashate lint.sh", b), "bashate lint.sh");
|
|
132
|
+
assert.equal(launchable("npm test", b), "npm test");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Сторож одной точки: команда гейта, запущенная оболочкой В ОБХОД gateCommand, на Windows снова
|
|
136
|
+
// уедет в WSL. Пять мест запуска починены разом; шестое, добавленное через месяц, забудет.
|
|
137
|
+
test("каждый запуск команды через оболочку идёт через gateCommand", async () => {
|
|
138
|
+
const { readdir, readFile } = await import("node:fs/promises");
|
|
139
|
+
const { join, dirname } = await import("node:path");
|
|
140
|
+
const { fileURLToPath } = await import("node:url");
|
|
141
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
142
|
+
const bad = [];
|
|
143
|
+
for (const dir of ["lib", "commands"]) {
|
|
144
|
+
for (const f of (await readdir(join(root, dir))).filter((n) => n.endsWith(".mjs"))) {
|
|
145
|
+
const text = await readFile(join(root, dir, f), "utf8");
|
|
146
|
+
for (const m of text.matchAll(/spawnSync\(([^,]+),\s*\{[^}]*shell:\s*true/g)) {
|
|
147
|
+
if (!m[1].trim().startsWith("gateCommand(")) bad.push(`${dir}/${f}: spawnSync(${m[1].trim()}, …)`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
assert.deepEqual(bad, [], "запуск в обход gateCommand");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Живьём, на настоящем Windows: модульные проверки выше подставляют пути, а не запускают. Без
|
|
155
|
+
// этой строки «windows: pass» в конвейере ничего не говорил о подмене — задание не гоняло ни
|
|
156
|
+
// этот файл, ни гейты через комплект. Кавычки в команде двойные: cmd.exe одинарные не снимает.
|
|
157
|
+
test("Windows: команда гейта со словом bash отвечает из Git Bash", { skip: process.platform !== "win32" && "только на Windows" }, async () => {
|
|
158
|
+
const { spawnSync } = await import("node:child_process");
|
|
159
|
+
const bash = gitBash();
|
|
160
|
+
assert.ok(bash, "Git Bash не найден рядом с git.exe");
|
|
161
|
+
assert.doesNotMatch(bash, /\\(system32|windowsapps)\\/i, "найдена заглушка WSL, а не Git Bash");
|
|
162
|
+
const r = spawnSync(gateCommand('bash -c "uname -s"'), { shell: true, encoding: "utf8" });
|
|
163
|
+
assert.equal(r.status, 0, r.stderr);
|
|
164
|
+
assert.match(r.stdout, /MINGW|MSYS/, `ответил не Git Bash: ${r.stdout}`);
|
|
165
|
+
});
|
|
@@ -12,6 +12,30 @@ 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
14
|
import { pickLang, langFromText } from "../i18n/index.mjs";
|
|
15
|
+
import { progress, selectGates } from "../lib/run.mjs";
|
|
16
|
+
|
|
17
|
+
// --- строка «идёт» во время прогона ---------------------------------------------------
|
|
18
|
+
// ЗАЧЕМ. Гейт идёт через spawnSync, и строка про него печаталась только по завершении: минута
|
|
19
|
+
// `smoke` — минута пустого экрана. Человек не отличает «работает» от «повисло» и пишет сам.
|
|
20
|
+
test("строка «идёт» пишется только в терминал и стирается без следа", () => {
|
|
21
|
+
const out = [];
|
|
22
|
+
const pipe = progress({ tty: false, write: (s) => out.push(s) });
|
|
23
|
+
pipe.show("⋯ smoke [2/10]");
|
|
24
|
+
pipe.clear();
|
|
25
|
+
assert.deepEqual(out, [], "в пайп и в конвейер — ни байта: лог читается глазами и разбирается машиной");
|
|
26
|
+
|
|
27
|
+
const term = [];
|
|
28
|
+
const p = progress({ tty: true, write: (s) => term.push(s) });
|
|
29
|
+
p.clear();
|
|
30
|
+
assert.deepEqual(term, [], "стирать нечего — управляющих кодов нет");
|
|
31
|
+
p.show("⋯ smoke [2/10]");
|
|
32
|
+
assert.ok(term.join("").includes("smoke [2/10]"));
|
|
33
|
+
assert.ok(!term.join("").includes("\n"), "без перевода строки: её перепишет итог гейта");
|
|
34
|
+
p.clear();
|
|
35
|
+
assert.equal(term.at(-1), "\r\x1b[K", "итог гейта ложится на чистую строку");
|
|
36
|
+
p.clear();
|
|
37
|
+
assert.equal(term.filter((s) => s === "\r\x1b[K").length, 1, "повторное стирание ничего не пишет");
|
|
38
|
+
});
|
|
15
39
|
|
|
16
40
|
// --- доказательство гейтов ------------------------------------------------------------
|
|
17
41
|
// ЗАЧЕМ. Ступень AQK-2 называлась «гейты доказаны» и проверяла существование двух папок.
|
|
@@ -225,7 +249,7 @@ test("заявка подтверждена, когда коды правил е
|
|
|
225
249
|
test("заявка не подтверждена, когда кодов нет нигде", () => {
|
|
226
250
|
const man = parseManifest('gates:\n lint: "ruff check ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
227
251
|
const catalog = [{ slug: "no-print-in-prod", recipes: { python: "ruff check --select T20 {dir}" } }];
|
|
228
|
-
assert.deepEqual(coversUnproven(man, catalog, ""), [{ entry: "no-print-in-prod", gate: "lint", codes: ["T20"] }]);
|
|
252
|
+
assert.deepEqual(coversUnproven(man, catalog, ""), [{ entry: "no-print-in-prod", gate: "lint", codes: ["T20"], linter: "ruff", kind: "unproven" }]);
|
|
229
253
|
});
|
|
230
254
|
|
|
231
255
|
// Правило может стоять не в команде, а в конфиге линтера — это нормальный уклад, и краснеть
|
|
@@ -243,6 +267,49 @@ test("запись без кодов правил в рецепте не пор
|
|
|
243
267
|
assert.deepEqual(coversUnproven(man, catalog, ""), []);
|
|
244
268
|
});
|
|
245
269
|
|
|
270
|
+
// --- заявка сверяется правилами ТОГО линтера, которым закрыт гейт --------------------
|
|
271
|
+
// Отзыв с живого проекта 2026-09-11 (TypeScript на Biome): заявка «lint держит no-print-in-prod»
|
|
272
|
+
// всегда была «не подтверждена» — комплект искал коды ruff (T20, C901, BLE), а у Biome это
|
|
273
|
+
// noConsole, noExcessiveCognitiveComplexity, noEmptyBlockStatements, и они стояли. Поле, которое
|
|
274
|
+
// должно снимать шум, само его производило и подталкивало ставить второй линтер.
|
|
275
|
+
const NP = { slug: "no-print-in-prod", biome_rules: "noConsole",
|
|
276
|
+
recipes: { python: "ruff check --select T20 {dir}", javascript: `eslint --rule '{"no-console":"error"}' {dir}` } };
|
|
277
|
+
const TODO = { slug: "todo-without-task", biome_rules: "none", recipes: { python: "ruff check --select FIX,TD {dir}" } };
|
|
278
|
+
|
|
279
|
+
test("Biome: правило записи стоит в biome.json — заявка подтверждена", () => {
|
|
280
|
+
const man = parseManifest('gates:\n lint: "npx biome check ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
281
|
+
const biome = '{"linter":{"rules":{"suspicious":{"noConsole":"error"}}}}';
|
|
282
|
+
assert.deepEqual(coversUnproven(man, [NP], { biome }), []);
|
|
283
|
+
const miss = coversUnproven(man, [NP], { biome: '{"linter":{"enabled":true}}' });
|
|
284
|
+
assert.equal(miss[0].kind, "unproven");
|
|
285
|
+
assert.deepEqual(miss[0].codes, ["noConsole"], "человеку названо ПРАВИЛО BIOME, а не код ruff");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("линтер гейта узнаётся и через npm-скрипт", () => {
|
|
289
|
+
const man = parseManifest('gates:\n lint: "npm run lint"\ncovers:\n lint: [no-print-in-prod]\n');
|
|
290
|
+
const scripts = { lint: "biome check ." };
|
|
291
|
+
const r = coversUnproven(man, [NP], { biome: '"noConsole": "error"', scripts });
|
|
292
|
+
assert.deepEqual(r, []);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("eslint: правило берётся из рецепта записи и ищется в конфиге eslint", () => {
|
|
296
|
+
const man = parseManifest('gates:\n lint: "eslint ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
297
|
+
assert.deepEqual(coversUnproven(man, [NP], { eslint: "rules: { 'no-console': 'error' }" }), []);
|
|
298
|
+
assert.equal(coversUnproven(man, [NP], { eslint: "rules: {}" })[0].kind, "unproven");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("у линтера нет такого правила вовсе — заявка заведомо неверна, а не «не подтверждена»", () => {
|
|
302
|
+
const man = parseManifest('gates:\n lint: "biome check ."\ncovers:\n lint: [todo-without-task]\n');
|
|
303
|
+
const r = coversUnproven(man, [TODO], { biome: "{}" });
|
|
304
|
+
assert.equal(r[0].kind, "impossible");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("линтер не распознан — «не умею проверить», а не обвинение", () => {
|
|
308
|
+
const man = parseManifest('gates:\n lint: "make lint"\ncovers:\n lint: [no-print-in-prod]\n');
|
|
309
|
+
const r = coversUnproven(man, [NP], {});
|
|
310
|
+
assert.equal(r[0].kind, "unknown");
|
|
311
|
+
});
|
|
312
|
+
|
|
246
313
|
// --- строка манифеста, которую разбор не понял, не исчезает молча ---------------
|
|
247
314
|
// Найдено 2026-09-09 случайно: подсаживал падающий гейт с именем «плохой», чтобы посмотреть
|
|
248
315
|
// на строку присутствия, — и прогон вышел с НУЛЁМ. Гейт не упал: его вообще не было. Разбор
|
|
@@ -297,3 +364,34 @@ test("сбой арбитра отнимает ступень, законная
|
|
|
297
364
|
|
|
298
365
|
assert.equal(verdict([{ state: "proven" }, { state: "broken" }]).ok, false);
|
|
299
366
|
});
|
|
367
|
+
|
|
368
|
+
// --- группы гейтов и --only / --skip ----------------------------------------------------
|
|
369
|
+
// ЗАЧЕМ. Отзыв с живого проекта 2026-09-11: гейт цены меряет план на засеянной базе. Объявишь
|
|
370
|
+
// его — он валит прогон без стенда; уберёшь — promise-has-gate справедливо ругается. Пришлось
|
|
371
|
+
// выкручиваться через advisory. Прогон бывал только «всё или ничего».
|
|
372
|
+
test("выбор гейтов: --skip группой и именем, --only сужает, пропущенные названы", () => {
|
|
373
|
+
const man = parseManifest('gates:\n lint: "x"\n unit: "x"\n cost: "x"\n e2e: "x"\ngroups:\n stack: [cost, e2e]\n');
|
|
374
|
+
const names = Object.entries(man.gates);
|
|
375
|
+
const s = selectGates(names, man, { skip: ["stack"] });
|
|
376
|
+
assert.deepEqual(s.run.map(([n]) => n), ["lint", "unit"]);
|
|
377
|
+
assert.deepEqual(s.skipped, ["cost", "e2e"]);
|
|
378
|
+
const o = selectGates(names, man, { only: ["lint", "cost"] });
|
|
379
|
+
assert.deepEqual(o.run.map(([n]) => n), ["lint", "cost"]);
|
|
380
|
+
assert.deepEqual(o.skipped, ["unit", "e2e"]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("выбор гейтов: неизвестное имя — ошибка, а не тихое «пропустили ничего»", () => {
|
|
384
|
+
const man = parseManifest('gates:\n lint: "x"\ngroups:\n stack: [lint]\n');
|
|
385
|
+
const s = selectGates(Object.entries(man.gates), man, { skip: ["stak"] });
|
|
386
|
+
assert.deepEqual(s.unknown, ["stak"]);
|
|
387
|
+
// Группа, в которой назван необъявленный гейт, — тоже названа: иначе группа молча пустеет.
|
|
388
|
+
const man2 = parseManifest('gates:\n lint: "x"\ngroups:\n stack: [cots]\n');
|
|
389
|
+
assert.deepEqual(selectGates(Object.entries(man2.gates), man2, { skip: ["stack"] }).unknown, ["cots"]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("выбор гейтов: без флагов гоняется всё, как раньше", () => {
|
|
393
|
+
const man = parseManifest('gates:\n a: "x"\n b: "x"\n');
|
|
394
|
+
const s = selectGates(Object.entries(man.gates), man, {});
|
|
395
|
+
assert.equal(s.run.length, 2);
|
|
396
|
+
assert.deepEqual(s.skipped, []);
|
|
397
|
+
});
|
|
@@ -4,6 +4,7 @@ import test from "node:test";
|
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { isFix, fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
|
|
6
6
|
import { probeableGates, gatesState, extAlternatives, planProbeGates, isCode } from "../commands/probe.mjs";
|
|
7
|
+
import { blindAdvice } from "../lib/advice.mjs";
|
|
7
8
|
|
|
8
9
|
// Признак починки берётся из ТЕМЫ коммита, а не из тела: тема — единственное, что пишут все,
|
|
9
10
|
// и единственное, что видно в `git log --oneline`. Три написания, потому что репозитории
|
|
@@ -220,6 +221,21 @@ test("парный вердикт: гейт позеленел от подсад
|
|
|
220
221
|
assert.equal(probeVerdictPaired(before, after).verdict, "unknown");
|
|
221
222
|
});
|
|
222
223
|
|
|
224
|
+
// Гейт работал ДО подсадки и сломался ОТ неё. Он мог быть тем самым ловцом — «никто не ловит»
|
|
225
|
+
// о нём сказать нельзя. Найдено пробой на самом комплекте 2026-09-11: образец лёг на место
|
|
226
|
+
// kit/gates/_skip.sh, общей библиотеки двенадцати гейтов, все двенадцать вышли с кодом 2 —
|
|
227
|
+
// включая gate-not-weakened, который в отдельной папке этот образец ловит. Остальные молчали,
|
|
228
|
+
// и проба назвала класс слепым; doctor повторил это человеку жирным.
|
|
229
|
+
test("парный вердикт: подсадка сломала работавший гейт — «не смогли», а не «слеп»", () => {
|
|
230
|
+
const { before, after } = pairs([["lint", 0], ["units", 0]], [["lint", 2], ["units", 0]]);
|
|
231
|
+
const r = probeVerdictPaired(before, after);
|
|
232
|
+
assert.equal(r.verdict, "unknown");
|
|
233
|
+
assert.equal(r.brokenByPlant, 1);
|
|
234
|
+
// Поимка соседом остаётся поимкой: знание получено, и сломанный гейт его не отменяет.
|
|
235
|
+
const other = pairs([["lint", 0], ["units", 0]], [["lint", 2], ["units", 1]]);
|
|
236
|
+
assert.equal(probeVerdictPaired(other.before, other.after).verdict, "caught");
|
|
237
|
+
});
|
|
238
|
+
|
|
223
239
|
test("парный вердикт: гейтов нет вовсе", () => {
|
|
224
240
|
assert.equal(probeVerdictPaired([], []).verdict, "unknown");
|
|
225
241
|
});
|
|
@@ -358,3 +374,91 @@ test("счёт непокрытого: исходы разложены по св
|
|
|
358
374
|
test("счёт непокрытого: пусто не роняет", () => {
|
|
359
375
|
assert.deepEqual(countProbe([]), { blindClasses: 0, caughtClasses: 0, unknownClasses: 0, probes: 0 });
|
|
360
376
|
});
|
|
377
|
+
|
|
378
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
// Непокрытый класс объясняется, а не называется. Написано ДО кода 2026-09-10.
|
|
380
|
+
//
|
|
381
|
+
// ЗАЧЕМ. Проба находит настоящие дыры — и печатает про них «close it: aqk add <имя>», то есть
|
|
382
|
+
// «поставь нашу штуку». Человек, который видит комплект впервые, закрывает окно.
|
|
383
|
+
//
|
|
384
|
+
// А готовая однострочная команда под его стек У НАС УЖЕ ЛЕЖИТ, в `recipes` записи каталога. Мы
|
|
385
|
+
// её просто не показываем. Замер руками на `requests` (самый скачиваемый python-пакет): в
|
|
386
|
+
// `src/requests/utils.py` — 75 коммитов-починок; дописана функция с `except Exception: pass`;
|
|
387
|
+
// их собственные `ruff` и `pytest` дали 0 и на чистой копии, и на подсаженной. Строка, которая
|
|
388
|
+
// бы это поймала, — `ruff check --select BLE,TRY400,SIM105 .` — лежала в нашем каталоге всё это
|
|
389
|
+
// время.
|
|
390
|
+
//
|
|
391
|
+
// Польза обязана быть видна ДО установки комплекта: скопировал строку, увидел находки у себя —
|
|
392
|
+
// и только тогда думаешь, ставить ли нас. Отчёт, который учит, а не отчитывается.
|
|
393
|
+
test("совет по непокрытому классу даёт команду под стек, а не «поставь нас»", () => {
|
|
394
|
+
const entry = {
|
|
395
|
+
slug: "swallowed-error",
|
|
396
|
+
recipes: {
|
|
397
|
+
any: "bash {gate}/check.sh {dir}",
|
|
398
|
+
python: "ruff check --select BLE,TRY400,SIM105 {dir}",
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
const a = blindAdvice(entry, { langs: new Set(["python"]) }, { file: "src/requests/utils.py", fixes: 75 });
|
|
402
|
+
assert.equal(a.command, "ruff check --select BLE,TRY400,SIM105 .",
|
|
403
|
+
"команда обязана быть готовой к вставке: {dir} подставлен, {gate} не годится");
|
|
404
|
+
assert.equal(a.file, "src/requests/utils.py");
|
|
405
|
+
assert.equal(a.fixes, 75);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Переносимый рецепт зовёт наш файл — вставить его человеку, у которого комплекта нет, нельзя.
|
|
409
|
+
// Тогда команды нет, и совет остаётся один: поставить запись.
|
|
410
|
+
test("совет: переносимый рецепт вставить некуда — команда не выдумывается", () => {
|
|
411
|
+
const entry = { slug: "x", recipes: { any: "bash {gate}/check.sh {dir}" } };
|
|
412
|
+
assert.equal(blindAdvice(entry, { langs: new Set(["python"]) }, { file: "a.py", fixes: 1 }).command, null);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("совет: у стека нет своего рецепта — команды нет", () => {
|
|
416
|
+
const entry = { slug: "x", recipes: { python: "ruff check {dir}" } };
|
|
417
|
+
assert.equal(blindAdvice(entry, { langs: new Set(["go"]) }, { file: "a.go", fixes: 2 }).command, null);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Python-проекту показывалась ссылка на eslint и knip: поле `tool` у записи общее на все языки.
|
|
421
|
+
// Совет, в котором половина не про тебя, читается весь как «не про тебя».
|
|
422
|
+
test("совет: адрес инструмента — того, которым начинается команда", () => {
|
|
423
|
+
const entry = {
|
|
424
|
+
slug: "dead-code",
|
|
425
|
+
tool: "https://github.com/jendrikseipp/vulture · https://github.com/webpro-nl/knip",
|
|
426
|
+
recipes: { python: "vulture --min-confidence 60 {dir}", javascript: "npx knip" },
|
|
427
|
+
};
|
|
428
|
+
assert.equal(blindAdvice(entry, { langs: new Set(["python"]) }).tool, "https://github.com/jendrikseipp/vulture");
|
|
429
|
+
// Команды нет или имя не совпало — весь список: лучше лишняя ссылка, чем ни одной.
|
|
430
|
+
assert.equal(blindAdvice(entry, { langs: new Set(["go"]) }).tool, entry.tool);
|
|
431
|
+
assert.equal(blindAdvice({ slug: "x" }, { langs: new Set() }).tool, null);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("совет: пустая запись не роняет разбор", () => {
|
|
435
|
+
assert.equal(blindAdvice({}, { langs: new Set() }, {}).command, null);
|
|
436
|
+
assert.equal(blindAdvice({}, {}, {}).command, null);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Безъязыковой родной рецепт годится в совет так же, как языковой: секреты ищутся в любом
|
|
440
|
+
// файле, и именно они чаще всего оказывались в непокрытых на чужих проектах.
|
|
441
|
+
test("совет: безъязыковой родной рецепт тоже даёт команду", () => {
|
|
442
|
+
const entry = { slug: "secrets-not-in-code",
|
|
443
|
+
recipes: { native: "gitleaks dir --no-banner {dir}", any: "bash {gate}/check.sh {dir}" } };
|
|
444
|
+
const a = blindAdvice(entry, { langs: new Set(["python"]) }, { file: "a.py", fixes: 9 });
|
|
445
|
+
assert.equal(a.command, "gitleaks dir --no-banner .");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Из совета вычищается то, что относится к НАМ, а не к его проекту. Исключение наших красных
|
|
449
|
+
// образцов (`gates/*/red`) нужно установленному гейту — рядом с ним лежат образцы. Человеку,
|
|
450
|
+
// который команду только копирует, эти флаги бессмысленны и подрывают доверие: он видит, что
|
|
451
|
+
// инструмент говорит про какие-то чужие каталоги, которых у него нет.
|
|
452
|
+
test("совет: исключения наших образцов в команду не попадают", () => {
|
|
453
|
+
const entry = { slug: "no-print-in-prod", recipes: { javascript:
|
|
454
|
+
"eslint --no-config-lookup --ignore-pattern 'gates/*/red/**' --ignore-pattern 'gates/*/green/**' --rule '{\"no-console\":\"error\"}' {dir}" } };
|
|
455
|
+
const a = blindAdvice(entry, { langs: new Set(["javascript"]) }, {});
|
|
456
|
+
assert.ok(!a.command.includes("gates/"), `в совете остались наши каталоги: ${a.command}`);
|
|
457
|
+
assert.ok(a.command.includes("no-console"), "правило потерялось вместе с исключениями");
|
|
458
|
+
assert.ok(a.command.startsWith("eslint "), a.command);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("совет: команда без наших исключений не портится", () => {
|
|
462
|
+
const entry = { slug: "x", recipes: { python: "ruff check --select T20 {dir}" } };
|
|
463
|
+
assert.equal(blindAdvice(entry, { langs: new Set(["python"]) }, {}).command, "ruff check --select T20 .");
|
|
464
|
+
});
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec } from "../lib/repo.mjs";
|
|
12
|
+
import { startWith } from "../lib/advice.mjs";
|
|
13
|
+
import { proposeGates } from "../lib/adopt.mjs";
|
|
12
14
|
import { CATALOGS, L } from "../i18n/index.mjs";
|
|
13
15
|
import { dirname } from "node:path";
|
|
14
16
|
|
|
@@ -162,3 +164,147 @@ test("у каждого признака репозитория есть объ
|
|
|
162
164
|
// запись каталога, выключенная навсегда и молча. Сторожим ту сторону, которая ломает.
|
|
163
165
|
}
|
|
164
166
|
});
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// Чужие проверки, которые у проекта УЖЕ есть. Написано ДО кода 2026-09-10.
|
|
170
|
+
//
|
|
171
|
+
// ЗАЧЕМ. Поставил комплект в `express` — проект с eslint, mocha и конвейером — и первое, что он
|
|
172
|
+
// увидел: двадцать крестов подряд и «держит машина 0». Это неправда с его точки зрения: его
|
|
173
|
+
// проверки держат, просто мы считаем только СВОИ записи.
|
|
174
|
+
//
|
|
175
|
+
// Мы видим, что конвейер ЕСТЬ (`has_ci`), но не читаем, что в нём. Человек должен вручную
|
|
176
|
+
// переписать в манифест то, что мы могли прочитать сами. Отсюда и «не понимает, что хорошо»:
|
|
177
|
+
// хорошее у него уже есть, а мы о нём молчим.
|
|
178
|
+
//
|
|
179
|
+
// Предлагаем, а не объявляем: гейт, вписанный без спроса, — это чужое решение в чужом файле.
|
|
180
|
+
test("чужие проверки: из package.json берутся test, lint и проверка типов", () => {
|
|
181
|
+
const pkg = JSON.stringify({ scripts: {
|
|
182
|
+
test: "mocha", lint: "eslint .", typecheck: "tsc --noEmit",
|
|
183
|
+
build: "rollup -c", start: "node server.js", prepare: "husky",
|
|
184
|
+
} });
|
|
185
|
+
const got = proposeGates({ "package.json": pkg });
|
|
186
|
+
assert.deepEqual(got.map((g) => g.name).sort(), ["lint", "test", "typecheck"]);
|
|
187
|
+
assert.equal(got.find((g) => g.name === "test").cmd, "npm test");
|
|
188
|
+
assert.equal(got.find((g) => g.name === "lint").cmd, "npm run lint");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// `npm test` и `npm run test` — разные написания одного; берём каноничное. А `build` и `start`
|
|
192
|
+
// проверками не являются: они собирают и запускают, а не судят.
|
|
193
|
+
test("чужие проверки: сборка и запуск проверками не считаются", () => {
|
|
194
|
+
const pkg = JSON.stringify({ scripts: { build: "tsc", start: "node .", dev: "vite" } });
|
|
195
|
+
assert.deepEqual(proposeGates({ "package.json": pkg }), []);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Makefile — вторая по частоте точка входа, и в python-проектах чаще первая.
|
|
199
|
+
test("чужие проверки: цели Makefile тоже видны", () => {
|
|
200
|
+
const mk = "install:\n\tpip install -e .\n\ntest:\n\tpytest -q\n\nlint:\n\truff check .\n";
|
|
201
|
+
const got = proposeGates({ Makefile: mk });
|
|
202
|
+
assert.deepEqual(got.map((g) => g.name).sort(), ["lint", "test"]);
|
|
203
|
+
assert.equal(got.find((g) => g.name === "test").cmd, "make test");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("чужие проверки: нечего предложить — пустой список, а не выдумка", () => {
|
|
207
|
+
assert.deepEqual(proposeGates({}), []);
|
|
208
|
+
assert.deepEqual(proposeGates({ "package.json": "{ не json" }), []);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Источник называется: человек обязан видеть, ОТКУДА мы это взяли, иначе предложение
|
|
212
|
+
// неотличимо от нашей догадки.
|
|
213
|
+
test("чужие проверки: у каждого предложения назван источник", () => {
|
|
214
|
+
const got = proposeGates({ "package.json": JSON.stringify({ scripts: { test: "jest" } }) });
|
|
215
|
+
assert.equal(got[0].source, "package.json");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Python-проекты. Замер 2026-09-11 на восьми живых (requests, click, flask, httpx, black,
|
|
219
|
+
// fastapi, pydantic, rich): package.json нет ни у кого, Makefile у трёх — и пятеро из восьми
|
|
220
|
+
// слышали «у вас ничего нет». А `.pre-commit-config.yaml` лежит у семи, tox — у пяти (у click и
|
|
221
|
+
// flask в pyproject.toml), у httpx — исполняемые scripts/test и scripts/check.
|
|
222
|
+
// Куски ниже сняты с этих репозиториев, а не придуманы.
|
|
223
|
+
test("чужие проверки: pre-commit предлагается целиком, хуки названы поимённо", () => {
|
|
224
|
+
const cfg = "repos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n hooks:\n" +
|
|
225
|
+
" - id: check-yaml\n - id: end-of-file-fixer\n" +
|
|
226
|
+
" - repo: https://github.com/astral-sh/ruff-pre-commit\n hooks:\n - id: ruff-check\n";
|
|
227
|
+
const got = proposeGates({ ".pre-commit-config.yaml": cfg });
|
|
228
|
+
assert.equal(got.length, 1);
|
|
229
|
+
assert.equal(got[0].cmd, "pre-commit run --all-files");
|
|
230
|
+
assert.match(got[0].source, /ruff-check/, "человек видит, ЧТО там стоит, а не только файл");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("чужие проверки: из tox берутся окружения-проверки, а не матрица версий", () => {
|
|
234
|
+
const ini = "[tox]\nenvlist = py{310,311}-{default}\n\n[testenv]\ncommands = pytest\n\n" +
|
|
235
|
+
"[testenv:lint]\ncommands = ruff check .\n\n[testenv:docs]\ncommands = sphinx-build\n\n[testenv:{,ci-}pypy3]\n";
|
|
236
|
+
assert.deepEqual(proposeGates({ "tox.ini": ini }).map((g) => g.cmd), ["tox -e lint"]);
|
|
237
|
+
// click и flask держат tox в pyproject.toml, новым синтаксисом.
|
|
238
|
+
const py = "[tool.tox]\nenv_list = [\"py3\"]\n\n[tool.tox.env.style]\ncommands = []\n\n" +
|
|
239
|
+
"[tool.tox.env.typing]\ncommands = []\n\n[tool.tox.env.docs]\ncommands = []\n";
|
|
240
|
+
const got = proposeGates({ "pyproject.toml": py });
|
|
241
|
+
assert.deepEqual(got.map((g) => g.cmd).sort(), ["tox -e style", "tox -e typing"]);
|
|
242
|
+
assert.equal(got[0].source, "pyproject.toml");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("чужие проверки: исполняемые scripts/test и scripts/check — как у httpx", () => {
|
|
246
|
+
const got = proposeGates({ "scripts/test": "", "scripts/check": "", "scripts/publish": "" });
|
|
247
|
+
assert.deepEqual(got.map((g) => g.cmd).sort(), ["scripts/check", "scripts/test"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
// С чего начать: три записи вместо двадцати равнозначных крестов. Написано ДО кода.
|
|
252
|
+
//
|
|
253
|
+
// ЗАЧЕМ. После установки человек видит двадцать крестов одинаковой формы и не знает, за что
|
|
254
|
+
// взяться. Двадцать одинаковых требований — это ноль требований: закрывают первое попавшееся
|
|
255
|
+
// или не закрывают ничего.
|
|
256
|
+
//
|
|
257
|
+
// Порядок НЕ ПО НАШЕМУ ВКУСУ. Два признака, оба — факты, которые у нас уже есть:
|
|
258
|
+
// · запись родилась из настоящего отказа (`proof` ссылается на журнал шишек — тот же
|
|
259
|
+
// признак, которым каталог отделяет условную запись) — она про боль, которая случалась, а не про «хорошую практику»;
|
|
260
|
+
// · её можно закрыть ОДНОЙ ГОТОВОЙ КОМАНДОЙ — значит цена входа минутная.
|
|
261
|
+
// Сначала то, что и больно, и дёшево.
|
|
262
|
+
test("с чего начать: сперва рождённые из отказа и закрываемые одной командой", () => {
|
|
263
|
+
const list = [
|
|
264
|
+
{ slug: "praktika", proof: "хорошая практика", recipes: { any: "bash {gate}/c.sh {dir}" } },
|
|
265
|
+
{ slug: "bol-i-deshevo", proof: "incidents/README.md, 2026-09-01", recipes: { native: "gitleaks dir {dir}" } },
|
|
266
|
+
{ slug: "bol-no-dorogo", proof: "incidents/README.md, 2026-08-02", recipes: { any: "bash {gate}/c.sh {dir}" } },
|
|
267
|
+
{ slug: "deshevo", proof: "методичка", recipes: { python: "ruff check {dir}" } },
|
|
268
|
+
];
|
|
269
|
+
assert.deepEqual(startWith(list, { langs: new Set(["python"]) }, 3).map((e) => e.slug),
|
|
270
|
+
["bol-i-deshevo", "deshevo", "bol-no-dorogo"]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("с чего начать: список короче трёх не ломается", () => {
|
|
274
|
+
assert.deepEqual(startWith([], { langs: new Set() }, 3), []);
|
|
275
|
+
const one = [{ slug: "a", proof: "incidents/README.md", recipes: {} }];
|
|
276
|
+
assert.deepEqual(startWith(one, { langs: new Set() }, 3).map((e) => e.slug), ["a"]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Порядок обязан быть УСТОЙЧИВЫМ: одинаковый ввод — одинаковый ответ, иначе человек видит
|
|
280
|
+
// разный совет на двух прогонах подряд и перестаёт верить обоим.
|
|
281
|
+
test("с чего начать: при равенстве признаков порядок стабилен", () => {
|
|
282
|
+
const list = [
|
|
283
|
+
{ slug: "b", proof: "incidents/README.md", recipes: {} },
|
|
284
|
+
{ slug: "a", proof: "incidents/README.md", recipes: {} },
|
|
285
|
+
];
|
|
286
|
+
assert.deepEqual(startWith(list, { langs: new Set() }, 2).map((e) => e.slug), ["a", "b"]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Совет под линтер ПРОЕКТА, а не под язык. Отзыв с живого проекта 2026-09-11: «начни с этих
|
|
290
|
+
// трёх» советовал завести eslint проекту на Biome — комплект читал package.json и Makefile, но не
|
|
291
|
+
// biome.json. Команда для Biome — одно правило разово (`--only`), и `--error-on-warnings`
|
|
292
|
+
// обязателен: правило вне рекомендованных Biome ставит на «предупреждение», и без флага команда
|
|
293
|
+
// выходила с нулём, напечатав находку. Проверено на Biome 2.5.12.
|
|
294
|
+
test("совет проекту на Biome: команда Biome, а не eslint", async () => {
|
|
295
|
+
const { blindAdvice } = await import("../lib/advice.mjs");
|
|
296
|
+
const entry = {
|
|
297
|
+
slug: "no-print-in-prod", biome_rules: "suspicious/noConsole",
|
|
298
|
+
tool: "https://github.com/eslint/eslint",
|
|
299
|
+
recipes: { javascript: `eslint --no-config-lookup --rule '{"no-console":"error"}' {dir}` },
|
|
300
|
+
};
|
|
301
|
+
const biome = blindAdvice(entry, { langs: new Set(["typescript"]), has_biome: true });
|
|
302
|
+
assert.equal(biome.command, "npx @biomejs/biome lint --error-on-warnings --only=suspicious/noConsole .");
|
|
303
|
+
assert.equal(biome.tool, "https://github.com/biomejs/biome");
|
|
304
|
+
// Без biome.json — прежний совет под язык.
|
|
305
|
+
const plain = blindAdvice(entry, { langs: new Set(["javascript"]), has_biome: false });
|
|
306
|
+
assert.match(plain.command, /^eslint /);
|
|
307
|
+
// У Biome правила нет вовсе (`none`) — прежний совет, а не выдуманная команда Biome.
|
|
308
|
+
const none = blindAdvice({ ...entry, biome_rules: "none" }, { langs: new Set(["javascript"]), has_biome: true });
|
|
309
|
+
assert.match(none.command, /^eslint /);
|
|
310
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tool/selfcheck/units-verdict.mjs — вердикт пробы: «поймано» только ПО ДЕЛУ.
|
|
2
|
+
//
|
|
3
|
+
// ОТДЕЛЬНЫМ ФАЙЛОМ: units-probe.mjs у предела в 500 строк, и шов настоящий — там разбор
|
|
4
|
+
// истории и план пробы, здесь то, на чём держится главное обещание пробы.
|
|
5
|
+
//
|
|
6
|
+
// ЗАЧЕМ. Отзыв с живого проекта 2026-09-11 (Amplifie, TypeScript на Biome): класс «цвет из
|
|
7
|
+
// токена темы» проба отметила пойманным линтером, хотя Biome цвета не проверяет. Подсаженный
|
|
8
|
+
// кусок сломал форматирование — упал форматтер. Вердикт считал «покраснел» равным «поймал»:
|
|
9
|
+
// код возврата сравнивался, вывод выбрасывался. Итог — «every applicable class is caught by
|
|
10
|
+
// something», ложная уверенность ровно в сторону самоуспокоения. К себе комплект был мягче,
|
|
11
|
+
// чем к проекту: от чужого гейта мы требуем покраснеть НА БРАКЕ, а свой вердикт этого не
|
|
12
|
+
// проверял.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { namesPlant, probeVerdictPaired, catchVerdict } from "../lib/history.mjs";
|
|
16
|
+
|
|
17
|
+
test("назвал подсаженный файл: путь появился в выводе после подсадки", () => {
|
|
18
|
+
assert.equal(namesPlant("", "src/ui/card.tsx:12:5 lint/style/noColorLiteral", "src/ui/card.tsx"), true);
|
|
19
|
+
// Любая форма пути: относительная с ./, абсолютная из песочницы, с обратными слешами Windows.
|
|
20
|
+
assert.equal(namesPlant("", "./src/ui/card.tsx:3 print found", "src/ui/card.tsx"), true);
|
|
21
|
+
assert.equal(namesPlant("", "/tmp/aqk-probe-x/src/ui/card.tsx:3", "src/ui/card.tsx"), true);
|
|
22
|
+
assert.equal(namesPlant("", "src\\ui\\card.tsx(3,1): error", "src/ui/card.tsx"), true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("не назвал: упал по своей причине — форматтер, чужой файл, общий итог", () => {
|
|
26
|
+
assert.equal(namesPlant("", "Formatter would have printed the following content.\nFound 1 error.", "src/ui/card.tsx"), false);
|
|
27
|
+
assert.equal(namesPlant("", "src/other.tsx:1 error", "src/ui/card.tsx"), false);
|
|
28
|
+
// Файл упоминался и до подсадки столько же раз — нового ничего не сказано.
|
|
29
|
+
assert.equal(namesPlant("src/ui/card.tsx:1 warn", "src/ui/card.tsx:1 warn", "src/ui/card.tsx"), false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const pairs = (before, after) => ({
|
|
33
|
+
before: before.map(([name, code]) => ({ name, code })),
|
|
34
|
+
after: after.map(([name, code, named]) => ({ name, code, named })),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("покраснел, но подсаженного файла не назвал — «неизвестно», а не «поймано»", () => {
|
|
38
|
+
const { before, after } = pairs([["lint", 0], ["units", 0]], [["lint", 1, false], ["units", 0]]);
|
|
39
|
+
const r = probeVerdictPaired(before, after);
|
|
40
|
+
assert.equal(r.verdict, "unknown");
|
|
41
|
+
assert.equal(r.unattributed, 1);
|
|
42
|
+
assert.equal(r.caught, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("поймал по делу хоть один — «поймано»; безымянное падение соседа этого не отменяет", () => {
|
|
46
|
+
const { before, after } = pairs([["fmt", 0], ["lint", 0]], [["fmt", 1, false], ["lint", 1, true]]);
|
|
47
|
+
const r = probeVerdictPaired(before, after);
|
|
48
|
+
assert.equal(r.verdict, "caught");
|
|
49
|
+
assert.equal(r.caught, 1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("без сведений о выводе (старые вызовы) поведение прежнее", () => {
|
|
53
|
+
const { before, after } = pairs([["lint", 0]], [["lint", 1, undefined]]);
|
|
54
|
+
assert.equal(probeVerdictPaired(before, after).verdict, "caught");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ИМЕНИ ФАЙЛА МАЛО. Biome, падая на форматировании, тоже называет файл: «src/card.tsx format».
|
|
58
|
+
// Отличить «упал на браке» от «упал на подсадке» можно только ПАРОЙ — ровно тем, чем мы проверяем
|
|
59
|
+
// чужие гейты: в то же место кладётся ЗЕЛЁНЫЙ образец той же записи. Краснеет и на нём — гейт
|
|
60
|
+
// падает от самой подсадки (форматирование, синтаксис), поимка не доказана.
|
|
61
|
+
test("пара: на красном назвал файл, на зелёном молчит — поймал", () => {
|
|
62
|
+
const red = { code: 1, out: "src/card.tsx:3 lint/noColorLiteral" };
|
|
63
|
+
const green = { code: 0, out: "" };
|
|
64
|
+
assert.equal(catchVerdict("", red, green, "src/card.tsx"), "caught");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("пара: краснеет и на зелёном, называя тот же файл, — падает от подсадки, не поймал", () => {
|
|
68
|
+
const red = { code: 1, out: "src/card.tsx format ━━━ Formatter would have printed" };
|
|
69
|
+
const green = { code: 1, out: "src/card.tsx format ━━━ Formatter would have printed" };
|
|
70
|
+
assert.equal(catchVerdict("", red, green, "src/card.tsx"), "planting");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("пара: файла не назвал — «безымянно»; зелёного образца под расширение нет — судим по имени", () => {
|
|
74
|
+
assert.equal(catchVerdict("", { code: 1, out: "Found 1 error." }, { code: 0, out: "" }, "src/a.py"), "nameless");
|
|
75
|
+
assert.equal(catchVerdict("", { code: 1, out: "src/a.py:1 T201" }, null, "src/a.py"), "caught");
|
|
76
|
+
});
|
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, browserServerAdvice, MARKS } from "../lib/repo.mjs";
|
|
15
|
+
import { triggerVerdict, recipeFor, pickRecipe, 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";
|
|
@@ -398,3 +398,36 @@ test("наборы файлов правил совпадают на обоих
|
|
|
398
398
|
|
|
399
399
|
|
|
400
400
|
|
|
401
|
+
|
|
402
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
403
|
+
// Рецепт, не привязанный к языку. Написано ДО кода 2026-09-10.
|
|
404
|
+
//
|
|
405
|
+
// ЗАЧЕМ. Ключи рецептов — языки: `python`, `javascript`, `go`. Это верно для линтеров, но
|
|
406
|
+
// неверно для инструментов, которым язык безразличен. `secrets-not-in-code` объявлен
|
|
407
|
+
// `always: true`, готовый аналог у него — `gitleaks`, и он ищет ключи в любом файле.
|
|
408
|
+
//
|
|
409
|
+
// Без такого ключа выбор один из двух, и оба плохие: продублировать `gitleaks` под восемь
|
|
410
|
+
// языков (тот самый повтор, против которого у нас есть гейт) или оставить запись без родного
|
|
411
|
+
// рецепта. Второе и было: замер на десяти проектах показал секреты в НЕПОКРЫТЫХ у axios и у
|
|
412
|
+
// всех четырёх python-проектов — то есть там, где польза наибольшая, команды для вставки не
|
|
413
|
+
// было.
|
|
414
|
+
//
|
|
415
|
+
// Порядок предпочтения: свой язык → безъязыковой родной → переносимый. Родной сильнее
|
|
416
|
+
// переносимого (у `gitleaks` сотни форматов токенов и чтение истории), но только если его есть
|
|
417
|
+
// чем выполнить: непоставленная программа даёт «not found», а это отсутствие сигнала, выданное
|
|
418
|
+
// за успех.
|
|
419
|
+
test("рецепт: безъязыковой родной берётся, когда своего языка нет", () => {
|
|
420
|
+
const rec = { recipes: { native: "true --scan", any: "bash {gate}/check.sh {dir}" } };
|
|
421
|
+
assert.equal(pickRecipe(rec, { langs: new Set(["python"]) }), "true --scan");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("рецепт: свой язык сильнее безъязыкового", () => {
|
|
425
|
+
const rec = { recipes: { python: "true --py", native: "true --scan", any: "bash x {dir}" } };
|
|
426
|
+
assert.equal(pickRecipe(rec, { langs: new Set(["python"]) }), "true --py");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Программы нет — родной не берётся ни в каком виде, иначе гейт встанет с «not found».
|
|
430
|
+
test("рецепт: безъязыковой без программы уступает переносимому", () => {
|
|
431
|
+
const rec = { recipes: { native: "aqk-nesuschestvuyuschiy --scan", any: "bash {gate}/check.sh {dir}" } };
|
|
432
|
+
assert.equal(pickRecipe(rec, { langs: new Set(["python"]) }), "bash {gate}/check.sh {dir}");
|
|
433
|
+
});
|