agent-quality-kit 0.6.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 +61 -2
- package/README.ru.md +61 -2
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +29 -4
- package/kit/gates/README.md +40 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-actually-fails/README.md +12 -0
- package/kit/gates/ci-actually-fails/check.sh +26 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- 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 +19 -3
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +27 -9
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +36 -5
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +10 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +14 -2
- package/kit/gates/gate-not-weakened/check.sh +13 -1
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/secrets-not-in-code/check.sh +29 -4
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/check.sh +13 -1
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +14 -2
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +9 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +22 -1
- package/package.json +6 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +72 -25
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +9 -1
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +37 -2
- package/tool/i18n/en-docs.mjs +154 -0
- package/tool/i18n/en.mjs +70 -90
- package/tool/i18n/ru-docs.mjs +156 -0
- package/tool/i18n/ru.mjs +69 -90
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +37 -2
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +63 -5
- package/tool/lib/prove.mjs +172 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +46 -2
- package/tool/lib/templates.mjs +3 -0
- package/tool/program.mjs +23 -22
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +519 -39
- 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 +122 -0
- package/tool/selfcheck/units.mjs +114 -2
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
package/tool/commands/doctor.mjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { scopeOutput, changedFiles } from "../lib/scope.mjs";
|
|
6
|
+
import { scopeOutput, splitAdvice, changedFiles } from "../lib/scope.mjs";
|
|
7
7
|
import { CWD, PKG_ROOT, TARGET_DIR, SELF, c, exists, die } from "../lib/core.mjs";
|
|
8
|
-
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS } from "../lib/manifest.mjs";
|
|
8
|
+
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, advisorySet, layoutChecks } from "../lib/manifest.mjs";
|
|
9
|
+
import { proveGates } from "../lib/prove.mjs";
|
|
9
10
|
import { detectFacts, readCatalog, triggerVerdict, recipeFor } from "../lib/repo.mjs";
|
|
10
11
|
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
11
12
|
import { L } from "../i18n/index.mjs";
|
|
@@ -108,6 +109,7 @@ function sinceRef(argv = process.argv) {
|
|
|
108
109
|
function runGates(man, opts = {}) {
|
|
109
110
|
const gates = declaredGates(man);
|
|
110
111
|
if (!gates.length) return { failed: 0, ran: 0, results: [] };
|
|
112
|
+
const advisory = advisorySet(man);
|
|
111
113
|
|
|
112
114
|
// Сужение по дифу — договор с человеком, и он должен видеть, ЧТО именно сужено. Пустой диф
|
|
113
115
|
// называется вслух: иначе «все гейты зелёные» означало бы «сравнили не с тем» и читалось бы
|
|
@@ -124,49 +126,83 @@ function runGates(man, opts = {}) {
|
|
|
124
126
|
const t0 = Date.now();
|
|
125
127
|
const r = spawnSync(cmd, { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
126
128
|
const secs = (Math.max(0, Date.now() - t0) / 1000).toFixed(1);
|
|
129
|
+
// Вывод гейта запоминается целиком (с потолком, чтобы болтливый инструмент не съел память):
|
|
130
|
+
// по нему считается покрытие дифа — какой файл вообще был назван хоть одной проверкой.
|
|
131
|
+
// Без этого «готово = доказано» остаётся правилом, за которым следит только человек.
|
|
132
|
+
const outAll = `${r.stdout || ""}${r.stderr || ""}`.slice(0, 200000);
|
|
127
133
|
|
|
128
134
|
if (r.error && r.error.code === "ETIMEDOUT") {
|
|
129
135
|
console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.timeout)}`);
|
|
130
136
|
failed++;
|
|
131
|
-
results.push({ name, cmd, ok: false, secs, note: L.doctor.timeout });
|
|
137
|
+
results.push({ name, cmd, ok: false, secs, note: L.doctor.timeout, out: outAll });
|
|
132
138
|
continue;
|
|
133
139
|
}
|
|
134
140
|
const code = r.status;
|
|
135
141
|
if (code === 0) {
|
|
136
142
|
console.log(` ${c.green("✔")} ${name.padEnd(14)} ${c.dim(`${secs}s · ${cmd}`)}`);
|
|
137
|
-
|
|
143
|
+
// Зелёный гейт иногда всё-таки говорит человеку что-то важное: храповик, дошедший до цели,
|
|
144
|
+
// просит убрать обёртку. Вывод успешного гейта не показывался вовсе, и это сообщение
|
|
145
|
+
// уходило в никуда — тот же класс, что обрезанный совет у красного, только тише.
|
|
146
|
+
// Показываем ровно строки с меткой совета: остальной вывод успешной проверки — шум.
|
|
147
|
+
const okAdvice = splitAdvice(`${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean)).advice;
|
|
148
|
+
for (const line of okAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
149
|
+
results.push({ name, cmd, ok: true, secs, out: outAll });
|
|
138
150
|
} else {
|
|
139
|
-
|
|
151
|
+
const raw = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
|
|
152
|
+
// Совет отделяется ДО сужения. Иначе он сам попадает под фильтр по путям: сообщение
|
|
153
|
+
// храповика про вышедший срок называет путь к реестру, реестра в дифе нет, и гейт,
|
|
154
|
+
// обязанный краснеть по сроку, печатался зелёным с пометкой «находки вне дифа».
|
|
155
|
+
// Ровно то, что стандарт запрещает: срок без последствия. Найдено ревью 2026-09-06.
|
|
156
|
+
const parted = splitAdvice(raw);
|
|
157
|
+
let out = parted.findings;
|
|
158
|
+
const alwaysAdvice = parted.advice;
|
|
140
159
|
|
|
141
160
|
// Сужение до дифа. Три исхода, и все три называются вслух.
|
|
142
161
|
if (scoped) {
|
|
143
162
|
const s = scopeOutput(out, scoped);
|
|
144
|
-
|
|
163
|
+
// Гейт, у которого находок нет вовсе, а есть только совет, сузить нечем: его вердикт
|
|
164
|
+
// не про файлы. Признать такой успешным — вернуть ту же тишину другим путём.
|
|
165
|
+
if (!s.scopable || out.length === 0) {
|
|
145
166
|
// Гейт печатает вердикт без путей — сузить нечем. Признать его успешным значило бы
|
|
146
167
|
// выдать провал за тишину; остаётся красным, и причина названа.
|
|
147
168
|
console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.exitCode(code))} ${c.dim(`· ${L.doctor.notScopable}`)}`);
|
|
148
169
|
failed++;
|
|
149
|
-
results.push({ name, cmd, ok: false, secs, code, note: L.doctor.notScopable });
|
|
170
|
+
results.push({ name, cmd, ok: false, secs, code, note: L.doctor.notScopable, out: outAll });
|
|
150
171
|
continue;
|
|
151
172
|
}
|
|
152
173
|
if (s.findings === 0) {
|
|
153
174
|
// Долг есть, но не в том, что внёс диф. Зелёный — но с числом спрятанного: молчаливое
|
|
154
175
|
// «всё хорошо» здесь было бы неправдой.
|
|
155
176
|
console.log(` ${c.green("✔")} ${name.padEnd(14)} ${c.dim(`${secs}s · ${L.doctor.outsideDiff(out.length)}`)}`);
|
|
156
|
-
results.push({ name, cmd, ok: true, secs, scopedAway: out.length });
|
|
177
|
+
results.push({ name, cmd, ok: true, secs, scopedAway: out.length, out: outAll });
|
|
157
178
|
continue;
|
|
158
179
|
}
|
|
159
180
|
out = s.kept;
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
failed++;
|
|
163
|
-
|
|
184
|
+
// Находки обрезаются, совет — никогда. Все записи каталога печатают «почини: …» последней
|
|
185
|
+
// строкой, и при обрезке до трёх строк человек не видел именно её: находка без действия
|
|
186
|
+
// закрывает окно, а не дефект.
|
|
187
|
+
// Совещательный гейт показывает находки и не роняет прогон. Знак другой, чтобы «показано»
|
|
188
|
+
// и «провалено» не читались одинаково; в сводке ниже он назван поимённо.
|
|
189
|
+
const isAdvisory = advisory.has(name);
|
|
190
|
+
if (isAdvisory) failed--;
|
|
191
|
+
const mark = isAdvisory ? c.yellow("!") : c.red("✘");
|
|
192
|
+
const verdict = isAdvisory ? c.yellow(L.doctor.advisoryMark) : c.red(L.doctor.exitCode(code));
|
|
193
|
+
console.log(` ${mark} ${name.padEnd(14)} ${verdict} ${c.dim(`· ${secs}s · ${cmd}`)}`);
|
|
164
194
|
for (const line of out.slice(0, 3)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
165
195
|
if (out.length > 3) console.log(c.dim(` ${L.doctor.moreLines(out.length - 3)}`));
|
|
166
|
-
|
|
196
|
+
// Совет тоже не бесконечен: гейт, зовущий помощник шесть раз, печатает его шесть раз.
|
|
197
|
+
for (const line of alwaysAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
198
|
+
results.push({ name, cmd, ok: false, secs, code, advisory: isAdvisory, out: outAll });
|
|
167
199
|
}
|
|
168
200
|
}
|
|
169
|
-
|
|
201
|
+
// Совещательные, которые покраснели, называются вслух ВСЕГДА. Молчание о них — ровно та
|
|
202
|
+
// тишина, против которой построен стандарт: проверка выключена, а выглядит как её отсутствие.
|
|
203
|
+
const advisoryFailed = results.filter((x) => x.advisory).map((x) => x.name);
|
|
204
|
+
if (advisoryFailed.length) console.log(`\n ${c.yellow(L.doctor.advisorySummary(advisoryFailed))}`);
|
|
205
|
+
return { failed, ran: gates.length, results, advisoryFailed };
|
|
170
206
|
}
|
|
171
207
|
|
|
172
208
|
// Короткий отчёт «что из этого реально брали» — не для человека, а для агента в следующей
|
|
@@ -205,13 +241,10 @@ async function cmdDoctor() {
|
|
|
205
241
|
// а копия завтра разошлась бы с ними. Без этого различия `doctor` краснел на собственном
|
|
206
242
|
// репозитории и требовал разложить комплект в комплект.
|
|
207
243
|
const inKit = resolve(CWD) === resolve(PKG_ROOT);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
[".gitignore", L.doctor.gitignore],
|
|
213
|
-
[".git", L.doctor.git],
|
|
214
|
-
];
|
|
244
|
+
const man = await readManifest();
|
|
245
|
+
// Что именно проверять — решает манифест: где у ЭТОГО проекта правила, методички и точка
|
|
246
|
+
// входа. Литеральный список стоял здесь до 2026-09-08 и печатал кресты за сделанное.
|
|
247
|
+
const checks = layoutChecks(man, inKit);
|
|
215
248
|
|
|
216
249
|
let missing = 0;
|
|
217
250
|
for (const [path, what] of checks) {
|
|
@@ -220,8 +253,10 @@ async function cmdDoctor() {
|
|
|
220
253
|
console.log(` ${ok ? c.green("✔") : c.red("✘")} ${path.padEnd(22)} ${c.dim(what)}`);
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
// Команды в
|
|
224
|
-
|
|
256
|
+
// Команды в точке входа заполнены или остались пустыми заготовками? Файл берётся тот же,
|
|
257
|
+
// что проверен выше, — иначе проект на `CLAUDE.md` этой проверки не получал вовсе.
|
|
258
|
+
const entryFile = (Array.isArray(man?.entry) ? man.entry : []).find((e) => typeof e === "string" && e.trim())?.trim() || "AGENTS.md";
|
|
259
|
+
const agents = join(CWD, entryFile);
|
|
225
260
|
if (await exists(agents)) {
|
|
226
261
|
const text = await readFile(agents, "utf8");
|
|
227
262
|
const emptyCommands = (text.match(/^- [^:]+: ``$/gm) || []).length;
|
|
@@ -233,8 +268,6 @@ async function cmdDoctor() {
|
|
|
233
268
|
}
|
|
234
269
|
}
|
|
235
270
|
|
|
236
|
-
const man = await readManifest();
|
|
237
|
-
|
|
238
271
|
// Опечатка в имени поля означала «поля нет»: вердикт выдавался неверный, а причина молчала.
|
|
239
272
|
// Называем поле и говорим, какие бывают — иначе человек ищет ошибку в проекте, а она в файле.
|
|
240
273
|
const unknown = unknownKeys(man);
|
|
@@ -243,12 +276,20 @@ async function cmdDoctor() {
|
|
|
243
276
|
console.log(c.dim(` ${L.doctor.manifestKnown(KNOWN_KEYS)}\n`));
|
|
244
277
|
}
|
|
245
278
|
|
|
246
|
-
|
|
279
|
+
// Доказательство считается только при прогоне: узнать, ловит ли гейт брак, нельзя иначе как
|
|
280
|
+
// запустив его по образцу. Без прогона ступени со второй помечаются «не доказано» — это
|
|
281
|
+
// честнее, чем показывать их выполненными по наличию папок.
|
|
282
|
+
const proof = process.argv.includes("--run") ? await proveGates(man) : null;
|
|
283
|
+
const { reached, steps } = await assessLevel(man, proof);
|
|
247
284
|
|
|
248
285
|
console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
|
|
249
286
|
for (const s of steps) {
|
|
250
287
|
const mark = s.ok ? c.green("✔") : reached + 1 === s.level ? c.yellow("→") : c.dim("·");
|
|
251
|
-
|
|
288
|
+
const note = !s.ok && s.needsProof ? c.dim(` · ${L.doctor.levelUnproven(`${SELF} prove`)}`) : "";
|
|
289
|
+
console.log(` ${mark} AQK-${s.level} ${s.title}${note}`);
|
|
290
|
+
}
|
|
291
|
+
if (proof && proof.broken) {
|
|
292
|
+
console.log(c.red(`\n ${L.doctor.gatesDoNotCatch(proof.broken, `${SELF} prove`)}`));
|
|
252
293
|
}
|
|
253
294
|
|
|
254
295
|
const next = steps.find((s) => !s.ok);
|
|
@@ -276,6 +317,12 @@ async function cmdDoctor() {
|
|
|
276
317
|
|
|
277
318
|
const facts = await detectFacts(man);
|
|
278
319
|
if (process.argv.includes("--baseline")) {
|
|
320
|
+
// `--baseline` — осмотр, а не прогон: он выходит с нулём всегда. Совмещённый с `--run` или
|
|
321
|
+
// `--min` он давал конвейер, который НЕ МОЖЕТ покраснеть: порог назван, гейты не запущены,
|
|
322
|
+
// код нулевой. Человек, собравший такую строку, считает, что порог держится. Отказываемся
|
|
323
|
+
// вслух — молчаливое зелёное здесь дороже сломанной команды. Найдено ревью 2026-09-08.
|
|
324
|
+
const clash = ["--run", "--min"].filter((f) => process.argv.includes(f));
|
|
325
|
+
if (clash.length) die(L.doctor.baselineClash(clash.join(", ")));
|
|
279
326
|
await reportBaseline(man, facts);
|
|
280
327
|
process.exit(0);
|
|
281
328
|
}
|
|
@@ -319,4 +366,4 @@ async function cmdDoctor() {
|
|
|
319
366
|
|
|
320
367
|
// Наружу — только команда. Остальное здесь же и используется: экспорт, который никто не
|
|
321
368
|
// импортирует, читается как «это часть договора» и мешает менять внутренности.
|
|
322
|
-
export { cmdDoctor, runGates, declaredGates };
|
|
369
|
+
export { cmdDoctor, runGates, declaredGates, sinceRef };
|
package/tool/commands/gates.mjs
CHANGED
|
@@ -47,7 +47,8 @@ async function installGate(slug, man, facts) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Команда под стек проекта, с путями внутри репозитория, а не внутри пакета.
|
|
50
|
-
const
|
|
50
|
+
const missing = [];
|
|
51
|
+
const picked = String(pickRecipe(rec, facts, missing) || "");
|
|
51
52
|
let cmd = picked
|
|
52
53
|
.replace(/\{gate\}/g, `${PROJECT_GATES}/${slug}`)
|
|
53
54
|
.replace(/\{dir\}/g, ".");
|
|
@@ -57,7 +58,7 @@ async function installGate(slug, man, facts) {
|
|
|
57
58
|
// ни `ruff`, ни `vulture`, и установка обрывалась на записи `dead-code`, которой нужен
|
|
58
59
|
// настоящий инструмент. Отсутствие сигнала неотличимо от успеха — здесь оно было внутри
|
|
59
60
|
// самой установки.
|
|
60
|
-
if (!cmd) return { rec, cmd: null, copied, declared: false, why: null, noRecipe: true };
|
|
61
|
+
if (!cmd) return { rec, cmd: null, copied, declared: false, why: null, noRecipe: true, missing };
|
|
61
62
|
|
|
62
63
|
// Родной инструмент не знает про наши образцы и выдаёт их как находки — в любом проекте,
|
|
63
64
|
// куда поставили гейты. Заворачиваем его в общий фильтр. Переносимая проверка фильтрует
|
|
@@ -94,9 +95,14 @@ async function cmdAdd(args) {
|
|
|
94
95
|
console.log(c.dim(` ${L.add.installAnyway}\n`));
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
const { cmd, copied, declared, why, noRecipe, retired } = await installGate(slug, man, facts);
|
|
98
|
+
const { cmd, copied, declared, why, noRecipe, retired, missing } = await installGate(slug, man, facts);
|
|
98
99
|
if (retired !== undefined) die(L.lifecycle.installDeprecated(slug, retired ? `${SELF} add ${retired}` : "—"));
|
|
99
|
-
|
|
100
|
+
// «Рецепта нет» и «рецепт есть, а инструмента нет» — разные причины и разные починки.
|
|
101
|
+
// Диагноз, противоречащий gate.yml, стоит доверия всему выводу. Найдено код-ревью 2026-09-07.
|
|
102
|
+
if (noRecipe) {
|
|
103
|
+
if (missing && missing.length) die(L.add.toolMissing(slug, missing));
|
|
104
|
+
die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack, missing));
|
|
105
|
+
}
|
|
100
106
|
|
|
101
107
|
console.log(c.bold(`\naqk add ${slug}\n`));
|
|
102
108
|
console.log(` ${c.green("✔")} ${PROJECT_GATES}/${slug}/ ${c.dim(L.add.copied(copied.length))}`);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// tool/commands/learn.mjs — кандидаты в правила из локальных логов сессий.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ. Тезис комплекта: обещание обязано стать командой. Но сначала обещание обязано быть
|
|
4
|
+
// ЗАПИСАНО, а половина того, что человек требует от агента, живёт только в переписке. Здесь
|
|
5
|
+
// комплект смотрит туда, где эти требования лежат, и показывает те, которых нет в точке входа.
|
|
6
|
+
//
|
|
7
|
+
// ЧТО ИЗМЕРЕНО ДО КОДА (2026-09-08, 67 сессий на машине владельца):
|
|
8
|
+
// · 25 353 записи `user` — из них человеком напечатано 1912. Остальное результаты
|
|
9
|
+
// инструментов. Отличает их поле `promptSource: "typed"`, и оно точнее любой эвристики:
|
|
10
|
+
// первая версия отбирала по длине и языку и выдавала «agent quality kit» 44 раза — то есть
|
|
11
|
+
// вставленные пути, а не правила;
|
|
12
|
+
// · из 1619 уникальных напечатанных реплик маркеры наставления дают 79, это 4%. Среди них
|
|
13
|
+
// настоящие правила («файл не трогай», «делай прогон с базой обязательно», «никаких
|
|
14
|
+
// обходных временных путей») и разговорная шелуха примерно поровну.
|
|
15
|
+
//
|
|
16
|
+
// ЧЕГО ЗДЕСЬ НАМЕРЕННО НЕТ. Поиска ПОВТОРОВ — приёма, на котором построен session-analyzer у
|
|
17
|
+
// agent-lint. Замер его не подтвердил: на 67 сессиях владелец не повторяет правило дословно, он
|
|
18
|
+
// говорит его один раз и каждый раз иначе. Те «повторы», что нашлись, оказались задвоением
|
|
19
|
+
// одной реплики в самом логе.
|
|
20
|
+
//
|
|
21
|
+
// ПРИВАТНОСТЬ. Команда читает переписку. Поэтому: только логи ТЕКУЩЕГО проекта (или явно
|
|
22
|
+
// названного), только в терминал, ни строки на диск, код возврата всегда 0. Отчёт, который
|
|
23
|
+
// можно закоммитить, из переписки не собирается — это решение, а не недоделка.
|
|
24
|
+
|
|
25
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { CWD, c, exists } from "../lib/core.mjs";
|
|
29
|
+
import { readManifest } from "../lib/manifest.mjs";
|
|
30
|
+
import { L } from "../i18n/index.mjs";
|
|
31
|
+
|
|
32
|
+
// Каталог логов зовётся по рабочему пути, где всё, кроме букв и цифр, заменено на дефис.
|
|
33
|
+
// Правило снято с живой машины, а не угадано: /home/ser/projects/audit_project лежит в
|
|
34
|
+
// -home-ser-projects-audit-project, то есть подчёркивание тоже становится дефисом.
|
|
35
|
+
function logSlug(cwd) {
|
|
36
|
+
return String(cwd).toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Маркеры наставления на двух языках. Список короткий намеренно: каждый лишний маркер добавляет
|
|
40
|
+
// шума больше, чем находок, а разбирать этот список человеку.
|
|
41
|
+
const MARKERS = new RegExp(
|
|
42
|
+
"(всегда|никогда|не надо|не нужно|обязательно|запомни|больше не|каждый раз|нельзя|" +
|
|
43
|
+
"только после|перед тем|сначала|не забывай|не трогай|как договорились|" +
|
|
44
|
+
"always|never|don'?t|do not|make sure|remember to|must not)",
|
|
45
|
+
"i",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Признаки вставки, а не реплики: длина, код в тройных кавычках, много переносов, пути, ссылки.
|
|
49
|
+
// Каждый добавлен по итогу прогона, а не на всякий случай.
|
|
50
|
+
function looksLikeRule(text) {
|
|
51
|
+
const t = String(text || "").trim();
|
|
52
|
+
if (!t || t.length > 400) return false;
|
|
53
|
+
if (t.includes("```")) return false;
|
|
54
|
+
if ((t.match(/\n/g) || []).length > 6) return false;
|
|
55
|
+
if (/https?:\/\//.test(t)) return false;
|
|
56
|
+
if ((t.match(/\S+\/\S+/g) || []).length >= 3) return false;
|
|
57
|
+
return MARKERS.test(t);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Слова, по которым сверяем сказанное с записанным. Короткие отброшены: на них совпадёт что
|
|
61
|
+
// угодно, и любое правило показалось бы уже записанным — то есть команда молчала бы всегда.
|
|
62
|
+
function keyWords(text) {
|
|
63
|
+
return [...new Set(String(text).toLowerCase().match(/[а-яёa-z]{4,}/g) || [])];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Сверяем по ОСНОВЕ, а не по слову целиком. Русский язык склоняет: в реплике «локальный
|
|
67
|
+
// костыль», в своде «до местного костыля» — по целому слову это промах, и правило, записанное
|
|
68
|
+
// час назад, показалось бы незаписанным. Проверено на живом логе: без основы первым же пунктом
|
|
69
|
+
// вышло правило, внесённое в AGENTS.md в тот же день.
|
|
70
|
+
function stem(w) {
|
|
71
|
+
return w.length > 5 ? w.slice(0, 5) : w;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Сказано вслух и НЕ записано. Порог половинный: правило, у которого хотя бы половина значимых
|
|
75
|
+
// слов уже стоит в точке входа, считаем записанным — иначе команда повторяла бы владельцу его
|
|
76
|
+
// же свод. Порог назван здесь, а не спрятан: он произвольный, и это видно.
|
|
77
|
+
function saidNotWritten(text, entryText) {
|
|
78
|
+
const words = keyWords(text);
|
|
79
|
+
if (!words.length) return false;
|
|
80
|
+
const hay = String(entryText || "").toLowerCase();
|
|
81
|
+
const hit = words.filter((w) => hay.includes(stem(w))).length;
|
|
82
|
+
return hit / words.length < 0.5;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Напечатанные человеком реплики одной сессии. Всё прочее — результаты инструментов, служебные
|
|
86
|
+
// вставки и подсказки — отбрасывается по полю promptSource.
|
|
87
|
+
function typedFrom(jsonl) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const line of jsonl.split("\n")) {
|
|
90
|
+
if (!line.trim()) continue;
|
|
91
|
+
let d;
|
|
92
|
+
try { d = JSON.parse(line); } catch { continue; }
|
|
93
|
+
if (d?.type !== "user" || d?.promptSource !== "typed") continue;
|
|
94
|
+
const cont = d?.message?.content;
|
|
95
|
+
const text = typeof cont === "string"
|
|
96
|
+
? cont
|
|
97
|
+
: Array.isArray(cont)
|
|
98
|
+
? cont.filter((b) => b?.type === "text").map((b) => b.text || "").join(" ")
|
|
99
|
+
: "";
|
|
100
|
+
const t = String(text).replace(/\s+/g, " ").trim();
|
|
101
|
+
if (t) out.push({ text: t, when: String(d.timestamp || "").slice(0, 10) });
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function readEntry(man) {
|
|
107
|
+
const names = Array.isArray(man?.entry) && man.entry.length ? man.entry : ["AGENTS.md", "CLAUDE.md"];
|
|
108
|
+
let all = "";
|
|
109
|
+
for (const n of names) {
|
|
110
|
+
try { all += `\n${await readFile(join(CWD, String(n)), "utf8")}`; } catch { /* нет файла — не беда */ }
|
|
111
|
+
}
|
|
112
|
+
return all;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function cmdLearn(argv = process.argv) {
|
|
116
|
+
const limitAt = argv.indexOf("--limit");
|
|
117
|
+
const limit = limitAt !== -1 && /^\d+$/.test(argv[limitAt + 1] || "") ? Number(argv[limitAt + 1]) : 20;
|
|
118
|
+
const root = join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "projects", logSlug(CWD));
|
|
119
|
+
|
|
120
|
+
console.log(c.bold(`\n ${L.learn.title}\n`));
|
|
121
|
+
if (!(await exists(root))) {
|
|
122
|
+
console.log(` ${L.learn.noLogs(root)}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let files = [];
|
|
127
|
+
try { files = (await readdir(root)).filter((f) => f.endsWith(".jsonl")); } catch { files = []; }
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const said = [];
|
|
130
|
+
let typedTotal = 0;
|
|
131
|
+
for (const f of files) {
|
|
132
|
+
let raw = "";
|
|
133
|
+
try { raw = await readFile(join(root, f), "utf8"); } catch { continue; }
|
|
134
|
+
for (const m of typedFrom(raw)) {
|
|
135
|
+
typedTotal++;
|
|
136
|
+
const key = m.text.toLowerCase().slice(0, 200);
|
|
137
|
+
if (seen.has(key)) continue;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
if (looksLikeRule(m.text)) said.push(m);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const entry = await readEntry(await readManifest());
|
|
144
|
+
const fresh = said.filter((m) => saidNotWritten(m.text, entry));
|
|
145
|
+
fresh.sort((a, b) => String(b.when).localeCompare(String(a.when)));
|
|
146
|
+
|
|
147
|
+
console.log(` ${c.dim(L.learn.counted(files.length, typedTotal, said.length, fresh.length))}\n`);
|
|
148
|
+
if (!fresh.length) {
|
|
149
|
+
console.log(` ${L.learn.nothing}\n`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const m of fresh.slice(0, limit)) {
|
|
153
|
+
console.log(` ${c.dim(m.when)} ${m.text.slice(0, 150)}`);
|
|
154
|
+
}
|
|
155
|
+
if (fresh.length > limit) console.log(c.dim(`\n ${L.learn.andMore(fresh.length - limit)}`));
|
|
156
|
+
console.log(`\n ${c.yellow(L.learn.warn)}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { cmdLearn, logSlug, looksLikeRule, saidNotWritten, typedFrom };
|
|
@@ -11,7 +11,7 @@ import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
|
|
|
11
11
|
import { readManifest } from "../lib/manifest.mjs";
|
|
12
12
|
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
13
13
|
import { installGate } from "./gates.mjs";
|
|
14
|
-
import { L } from "../i18n/index.mjs";
|
|
14
|
+
import { L, LANG } from "../i18n/index.mjs";
|
|
15
15
|
|
|
16
16
|
async function cmdInit(args) {
|
|
17
17
|
const force = args.includes("--force");
|
|
@@ -46,6 +46,13 @@ async function cmdInit(args) {
|
|
|
46
46
|
for (const f of created.slice(0, 8)) console.log(` ${f}`);
|
|
47
47
|
if (created.length > 8) console.log(c.dim(` ${L.init.andMore(created.length - 8)}`));
|
|
48
48
|
}
|
|
49
|
+
// Методички остаются на русском — решение владельца, принятое 2026-09-07, а не недоделка.
|
|
50
|
+
// Сказать об этом обязательно: человек, открывший `.aqk/docs/` и увидевший чужой язык, иначе
|
|
51
|
+
// решит, что установка сломалась. Правила переведены, методички нет; молчать об этом значит
|
|
52
|
+
// выдать решение за оплошность.
|
|
53
|
+
if (LANG === "en" && created.some((f) => f.includes(`${TARGET_DIR}/docs/`) || f.includes(`${TARGET_DIR}\\docs\\`))) {
|
|
54
|
+
console.log(c.dim(`\n ${L.init.docsRu}`));
|
|
55
|
+
}
|
|
49
56
|
if (skipped.length) {
|
|
50
57
|
console.log(c.yellow(`\n ${L.init.kept(skipped.length)}`));
|
|
51
58
|
for (const f of skipped) console.log(` ${f}`);
|
|
@@ -69,6 +76,7 @@ ${c.bold(L.init.nextTitle)}
|
|
|
69
76
|
4. ${L.init.n4a} ${c.bold(L.init.n4b)}${L.init.n4c}
|
|
70
77
|
${L.init.n4d}
|
|
71
78
|
|
|
79
|
+
${c.dim(L.init.hookHint(`${SELF} context --install`))}
|
|
72
80
|
${c.dim(L.init.burned(`${SELF} note "…"`))}
|
|
73
81
|
`);
|
|
74
82
|
await maybeAskFeedback();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// tool/commands/prove.mjs — `aqk prove`: доказать, что гейты проекта ловят брак.
|
|
2
|
+
//
|
|
3
|
+
// Отдельная команда, а не флаг: вопрос «работают ли мои проверки» задают сам по себе, и ответ
|
|
4
|
+
// на него нужен раньше, чем прогон по коду. Прогон говорит «сегодня чисто»; доказательство —
|
|
5
|
+
// «а если бы было грязно, я бы это увидел».
|
|
6
|
+
import { readManifest } from "../lib/manifest.mjs";
|
|
7
|
+
import { proveGates } from "../lib/prove.mjs";
|
|
8
|
+
import { c, SELF } from "../lib/core.mjs";
|
|
9
|
+
import { L } from "../i18n/index.mjs";
|
|
10
|
+
|
|
11
|
+
function line(r) {
|
|
12
|
+
const P = L.prove;
|
|
13
|
+
const pad = r.name.padEnd(22);
|
|
14
|
+
if (r.state === "proven") return ` ${c.green("✔")} ${pad} ${c.dim(P.okRed)}`;
|
|
15
|
+
if (r.state === "unprovable") {
|
|
16
|
+
const why =
|
|
17
|
+
r.why === "no-samples" ? P.noSamples
|
|
18
|
+
: r.why === "other-recipe" ? P.otherRecipe(r.forRecipe.lang)
|
|
19
|
+
: r.why === "no-target" ? P.noTarget
|
|
20
|
+
: P.empty;
|
|
21
|
+
return ` ${c.dim("~")} ${c.dim(pad)} ${c.dim(why)}`;
|
|
22
|
+
}
|
|
23
|
+
const why = r.why === "red-passed" ? P.redPassed : r.why === "green-failed" ? P.greenFailed : P.empty;
|
|
24
|
+
return ` ${c.red("✘")} ${pad} ${c.red(why)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function cmdProve() {
|
|
28
|
+
const man = await readManifest();
|
|
29
|
+
const P = L.prove;
|
|
30
|
+
console.log(c.bold(`\n${P.title}\n`));
|
|
31
|
+
|
|
32
|
+
// «Манифеста нет» и «гейтов не объявлено» — разные причины и разные починки. Остальные
|
|
33
|
+
// команды это различают; здесь не различалось. Найдено код-ревью 2026-09-07.
|
|
34
|
+
if (!man) {
|
|
35
|
+
console.log(` ${c.red(L.ratchet.noManifest(`${SELF} init`))}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
39
|
+
if (!Object.keys(gates).length) {
|
|
40
|
+
console.log(` ${P.noGates}\n`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (!String(man?.samples || "").trim()) {
|
|
44
|
+
console.log(` ${c.red(P.noSamplesDir)}\n`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const res = await proveGates(man);
|
|
49
|
+
// Сначала сломанные: красное называется первым, иначе его не читают.
|
|
50
|
+
for (const r of res.results.filter((x) => x.state === "broken")) console.log(line(r));
|
|
51
|
+
for (const r of res.results.filter((x) => x.state === "proven")) console.log(line(r));
|
|
52
|
+
for (const r of res.results.filter((x) => x.state === "unprovable")) console.log(line(r));
|
|
53
|
+
|
|
54
|
+
const parts = [c.green(P.proven(res.proven))];
|
|
55
|
+
if (res.broken) parts.push(c.red(P.broken(res.broken)));
|
|
56
|
+
if (res.unprovable) parts.push(c.dim(P.unprovable(res.unprovable)));
|
|
57
|
+
console.log(`\n ${parts.join(" ")}`);
|
|
58
|
+
|
|
59
|
+
if (!res.ok && res.proven === 0 && res.broken === 0) {
|
|
60
|
+
console.log(`\n ${c.yellow(P.nothingProven)}`);
|
|
61
|
+
console.log(` ${c.dim(P.fix(`${SELF} add ${L.help.name}`))}`);
|
|
62
|
+
}
|
|
63
|
+
console.log("");
|
|
64
|
+
process.exit(res.ok ? 0 : 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { cmdProve };
|
package/tool/commands/report.mjs
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
|
|
17
17
|
import { join, relative } from "node:path";
|
|
18
|
+
import { statSync } from "node:fs";
|
|
18
19
|
import { CWD, TARGET_DIR, SELF, c, exists, docPath } from "../lib/core.mjs";
|
|
19
20
|
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
21
|
+
import { proveGates } from "../lib/prove.mjs";
|
|
20
22
|
import { detectFacts, readCatalog, triggerVerdict, whichSync } from "../lib/repo.mjs";
|
|
21
|
-
import {
|
|
23
|
+
import { changedCode, coverage, evidenceHash, readForHash } from "../lib/evidence.mjs";
|
|
24
|
+
import { runGates, declaredGates, sinceRef } from "./doctor.mjs";
|
|
22
25
|
import { L } from "../i18n/index.mjs";
|
|
23
26
|
|
|
24
27
|
// Каким рецептом стоит гейт: родным инструментом или переносимой проверкой. Именно это
|
|
@@ -73,7 +76,9 @@ async function cmdReport() {
|
|
|
73
76
|
process.exit(1);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
// Значок — самое громкое утверждение комплекта, и доказывать его обязательно. Без этого
|
|
80
|
+
// проект с гейтом «true» получал AQK-3 и зелёную картинку в README: проверено прогоном.
|
|
81
|
+
const { reached } = await assessLevel(man, await proveGates(man));
|
|
77
82
|
const facts = await detectFacts(man);
|
|
78
83
|
const catalog = await readCatalog();
|
|
79
84
|
const bySlug = Object.fromEntries(catalog.map((r) => [r.slug, r]));
|
|
@@ -172,6 +177,36 @@ async function cmdReport() {
|
|
|
172
177
|
say(`> ${L.report2.ignoreWarn}`);
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
// --- чем доказан этот диф -------------------------------------------------
|
|
181
|
+
// Раздел появляется только с `--since`: без базы сравнения говорить о покрытии нечего, а
|
|
182
|
+
// молчаливо взять умолчание нельзя — «сравнили не с тем» неотличимо от «всё покрыто».
|
|
183
|
+
const since = sinceRef();
|
|
184
|
+
if (since) {
|
|
185
|
+
const changed = changedCode(since, CWD);
|
|
186
|
+
say("");
|
|
187
|
+
say(`## ${L.report2.evidenceTitle}`);
|
|
188
|
+
say("");
|
|
189
|
+
if (changed === null) {
|
|
190
|
+
say(`- ⚠️ ${L.report2.evidenceBadRef(since)}`);
|
|
191
|
+
} else if (!changed.length) {
|
|
192
|
+
say(`- ${L.report2.evidenceNoFiles(since)}`);
|
|
193
|
+
} else {
|
|
194
|
+
// Каталог ли это — спрашиваем у диска: цель гейта «tool» и файл «tool.js» иначе
|
|
195
|
+
// неразличимы, и второй попал бы в «просмотрен» ни за что.
|
|
196
|
+
const isDir = (rel) => { try { return statSync(join(CWD, rel)).isDirectory(); } catch { return false; } };
|
|
197
|
+
const cov = coverage(changed, run.results, isDir);
|
|
198
|
+
const hash = evidenceHash(since, run.results, readForHash(changed, CWD));
|
|
199
|
+
for (const [f, by] of cov.covered) say(`- ✅ ${f} — ${L.report2.evidenceNamed(by.join(", "))}`);
|
|
200
|
+
for (const [f, by] of cov.silent) say(`- ◻️ ${f} — ${L.report2.evidenceSilent(by.length)}`);
|
|
201
|
+
for (const f of cov.uncovered) say(`- ❌ ${f} — ${L.report2.evidenceUncovered}`);
|
|
202
|
+
say("");
|
|
203
|
+
say(`- ${L.report2.evidenceBase}: \`${since}\``);
|
|
204
|
+
say(`- ${L.report2.evidenceHash}: \`${hash}\``);
|
|
205
|
+
say("");
|
|
206
|
+
say(`> ${L.report2.evidenceWarn}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
175
210
|
say("");
|
|
176
211
|
say(`## ${L.report2.whyTitle}`);
|
|
177
212
|
say("");
|