agent-quality-kit 0.12.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 +26 -4
- package/README.ru.md +26 -4
- package/kit/gates/complexity-limit/gate.yml +4 -0
- package/kit/gates/dead-code/gate.yml +4 -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 +4 -0
- package/kit/gates/swallowed-error/gate.yml +4 -0
- package/kit/gates/todo-without-task/gate.yml +4 -0
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +56 -3
- package/tool/commands/doctor.mjs +136 -81
- package/tool/commands/probe.mjs +46 -66
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +15 -11
- package/tool/i18n/en-docs.mjs +22 -1
- package/tool/i18n/en-gates.mjs +6 -1
- package/tool/i18n/en.mjs +13 -1
- package/tool/i18n/ru-docs.mjs +24 -1
- package/tool/i18n/ru-gates.mjs +6 -1
- package/tool/i18n/ru.mjs +13 -1
- package/tool/lib/adopt.mjs +15 -1
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +11 -1
- package/tool/lib/core.mjs +39 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +34 -5
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/repo.mjs +2 -31
- package/tool/lib/run.mjs +78 -7
- 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 +15 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-level.mjs +76 -2
- package/tool/selfcheck/units-probe.mjs +2 -1
- package/tool/selfcheck/units-repo.mjs +25 -1
- package/tool/selfcheck/units-verdict.mjs +76 -0
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -66,7 +66,7 @@ const enDocs = {
|
|
|
66
66
|
runClean: (when) => `Last run ${when} — nothing red.`,
|
|
67
67
|
runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
|
|
68
68
|
andMore: (n) => `and ${n} more`,
|
|
69
|
-
skipped: (n) => `Not run: ${n} — the tool is absent on this machine
|
|
69
|
+
skipped: (n) => `Not run: ${n} — skipped by --only/--skip or the tool is absent on this machine; their state is unknown.`,
|
|
70
70
|
probeNever: "No coverage probe has run — what is covered by nothing here is UNKNOWN. That is not \"covered\": `aqk probe`.",
|
|
71
71
|
probeOff: "The coverage probe is switched off in the manifest (`probe: 0`) — nobody counts what is covered by nothing here.",
|
|
72
72
|
probeUnknown: "The coverage probe could not run — what is covered by nothing here is UNKNOWN. That is not \"covered\".",
|
|
@@ -79,6 +79,26 @@ const enDocs = {
|
|
|
79
79
|
(behind ? ` (${behind} commits behind)` : "") + ".",
|
|
80
80
|
ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
|
|
81
81
|
where: (entry) => `The rulebook: ${entry}. What proves a diff: \`aqk report --since main\`.`,
|
|
82
|
+
nextTitle: "Next, in order — computed from this repository, not generic advice:",
|
|
83
|
+
nextStep: {
|
|
84
|
+
init: () => "Set up the standard: `aqk init` — without .aqk.yml `aqk add` refuses.",
|
|
85
|
+
adopt: (s) => `Declare the checks this project already has: in .aqk.yml, under gates: ${s.gates.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Then \`aqk doctor --run\`.`,
|
|
86
|
+
blind: (s) => `The "${s.slug}" defect the probe planted in ${s.file} was NOT caught by your checks.` +
|
|
87
|
+
(s.command ? ` Catch it now: \`${s.command}\`.` : "") + ` Keep it caught: \`aqk add ${s.slug}\`.`,
|
|
88
|
+
start: (s) => `Install ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (one line, no kit needed: \`${s.command}\`)` : "") +
|
|
89
|
+
", then `aqk prove` — the gate must go red on its own red sample.",
|
|
90
|
+
},
|
|
91
|
+
nextMore: (n) => `And ${n} more — the full list: \`aqk doctor\`.`,
|
|
92
|
+
whenTitle: "When to do what:",
|
|
93
|
+
whenCommit: (hook) => hook === true
|
|
94
|
+
? "Before a commit → the `.git/hooks/pre-commit` hook runs the checks itself; do not bypass it (`--no-verify`)."
|
|
95
|
+
: "Before a commit → `aqk doctor --run --since main`: there is no pre-commit hook, it will not happen by itself.",
|
|
96
|
+
whenRules: [
|
|
97
|
+
"Added or changed a check → `aqk prove`: the gate must go red on its own red sample, otherwise it checks nothing.",
|
|
98
|
+
"Writing a rule into the rulebook → put the arbiter mark next to it, `<!-- aqk: gate-name -->`; no gate — `<!-- aqk: human -->`, which is an admission, not a check.",
|
|
99
|
+
"A check is in your way → do not weaken it (`|| true`, `--exit-zero`, a suppression without a rule code): stop and ask the owner.",
|
|
100
|
+
"You think it is done → `aqk report --since main`: every changed file must be named by a check.",
|
|
101
|
+
],
|
|
82
102
|
mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
|
|
83
103
|
rulesTitle: (e) => `THE RULEBOOK OF THIS PROJECT (${e}) — verbatim, in full. This is not an invitation\nto read it: it is already here.`,
|
|
84
104
|
hookAlready: (p) => `the hook is already in ${p} — changing nothing.`,
|
|
@@ -196,6 +216,7 @@ const enDocs = {
|
|
|
196
216
|
},
|
|
197
217
|
],
|
|
198
218
|
report: {
|
|
219
|
+
skippedBySelect: "not run (by --only/--skip), state unknown",
|
|
199
220
|
title: "aqk doctor --run",
|
|
200
221
|
version: "version",
|
|
201
222
|
level: "level",
|
package/tool/i18n/en-gates.mjs
CHANGED
|
@@ -158,6 +158,7 @@ export const enGates = {
|
|
|
158
158
|
"the guides in .aqk/docs/ are in Russian — a deliberate decision, not a broken install.\n The rules in .aqk/rules/ are in English; the guides are prose an agent may ignore anyway,\n and what a machine holds lives in .aqk.yml and the gates. Translation waits for someone who needs it.",
|
|
159
159
|
created: (n) => `created (${n}):`,
|
|
160
160
|
andMore: (n) => `… and ${n} more`,
|
|
161
|
+
ignored: (list) => `the kit's runtime files were added to .gitignore — machine state, it does not belong in git: ${list}`,
|
|
161
162
|
kept: (n) => `already there, left untouched (${n}):`,
|
|
162
163
|
overwrite: (cmd) => `overwrite: ${cmd}`,
|
|
163
164
|
nextTitle: "What to do next, in order:",
|
|
@@ -255,16 +256,20 @@ export const enGates = {
|
|
|
255
256
|
badEvery: (v) => `the manifest says probe: "${v}", which is not a commit count. The probe does NOT run: silently using the default would mean doing something other than what is written.`,
|
|
256
257
|
autoFirst: "no coverage probe has ever run here — running it myself. Turn off: AQK_PROBE=0",
|
|
257
258
|
auto: (n) => `${n} commits since the last probe — running it myself. Turn off: AQK_PROBE=0`,
|
|
259
|
+
autoNotInCi: (cmd) => `in CI the probe does not run by itself — it would be minutes of surprise in a fast check; run it as a separate job: ${cmd} (or AQK_PROBE=1)`,
|
|
258
260
|
title: "aqk probe — what the declared checks cannot see",
|
|
259
261
|
method: (files, entries, gates) =>
|
|
260
262
|
`method: a red sample from a catalogue entry is planted into a COPY of the project, then the ` +
|
|
261
263
|
`DECLARED gates are run there — the command is used as written, nothing is substituted into it. ` +
|
|
262
264
|
`Files: ${files}, applicable entries: ${entries}, gates green on a clean checkout: ${gates}. ` +
|
|
263
|
-
`
|
|
265
|
+
`Project files are not changed; the probe result goes to .aqk/last-probe.md (in .gitignore after init).`,
|
|
264
266
|
fixes: (n) => `fixes in history: ${n}`,
|
|
265
267
|
caught: (names) => `caught by: ${names}`,
|
|
266
268
|
blind: "NOTHING CATCHES IT",
|
|
267
269
|
unknown: "nothing to check with — the gate did not run (delegated tool missing)",
|
|
270
|
+
nameless: "did not name the planted file",
|
|
271
|
+
planting: "goes red on the green sample too — fails from the planting itself",
|
|
272
|
+
unattributed: (g) => `went red: ${g} — the catch is not proven`,
|
|
268
273
|
blindWhere: (file, fixes) => `where: ${file} — ${fixes} fix commits in its history`,
|
|
269
274
|
blindWhat: "what we planted into your file:",
|
|
270
275
|
blindFix: (cmd) => `catch it right now, no kit needed: ${cmd}`,
|
package/tool/i18n/en.mjs
CHANGED
|
@@ -44,6 +44,7 @@ export const en = {
|
|
|
44
44
|
blob: "assemble the guides into a single GOD_AI.md",
|
|
45
45
|
learn: "rule candidates from local transcripts: said out loud, never written down",
|
|
46
46
|
context: "the project state in one block — for an agent's context, not for reading",
|
|
47
|
+
vitals: "is what the kit runs on wired up: gate tools, hooks, version freshness",
|
|
47
48
|
contextInstall: "the same in full — the map and the rulebook — installed as a hook",
|
|
48
49
|
report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
|
|
49
50
|
badge: "a level badge for your README — and a check that it does not lie",
|
|
@@ -65,6 +66,14 @@ export const en = {
|
|
|
65
66
|
gitignore: "repository hygiene",
|
|
66
67
|
git: "project under version control",
|
|
67
68
|
|
|
69
|
+
runtimeTracked: (f, cmd) => `${f} is tracked by git — every run rewrites it, and the tree always shows a modified file. Take it out: ${cmd}`,
|
|
70
|
+
runtimeNotIgnored: (f, cmd) => `${f} is this machine's state, yet git sees it: one \`git add .\` and it is in a commit. Hide it: ${cmd}`,
|
|
71
|
+
layoutAdvice: "missing — advice, it does not fail the run",
|
|
72
|
+
coversImpossible: (entry, gate, linter) => `the claim "${gate} holds ${entry}" is wrong: ${linter} has no rule for this class — there is nothing to hold it with`,
|
|
73
|
+
coversCantCheck: (entry, gate) => `cannot check the claim "${gate} holds ${entry}": the gate's linter is not recognised or the entry has no rules for it — taken on trust`,
|
|
74
|
+
selectUnknown: (names, groups) => `--only/--skip: "${names}" is neither a gate from gates: nor a group from groups:${groups ? ` (groups: ${groups})` : ""}. Running everything instead of skipping would be a lie, so stopping.`,
|
|
75
|
+
selectSkipped: (names) => `not run (by --only/--skip): ${names} — their state is unknown, they are not "green"`,
|
|
76
|
+
jobsBad: (v) => `--jobs expects a whole number from 1: "${v}" will not do. A one-by-one run passed off as parallel would be a lie, so stopping.`,
|
|
68
77
|
rulesByHuman: (total, machine, human) =>
|
|
69
78
|
`${human} of ${total} rules in the entry point are guarded by a HUMAN, ${machine} by a machine.`,
|
|
70
79
|
rulesByHumanWhy: "A rule guarded by a human is guarded by nobody the day the human is busy. That is the hole this kit exists to close — and the reminder belongs to the human, not only to the agent. Counted in the ENTRY POINT only: a promise kept in any other file is checked by nothing at all, and this kit will not tell you it exists.",
|
|
@@ -108,7 +117,9 @@ export const en = {
|
|
|
108
117
|
blindRan: (g) => `gate ${g} is declared and was run — and still missed the defect in this file`,
|
|
109
118
|
blindInstalled: "declared, but the probe did not run this gate (added later or too slow) — the next probe will show whether it catches",
|
|
110
119
|
blindMore: (cmd) => `what was planted and where — ${cmd}`,
|
|
111
|
-
probeNever: (cmd) => `Whether your checks catch a real defect has not been tested yet: ${cmd} plants one in a copy of the project and shows (a minute or two, your files are not
|
|
120
|
+
probeNever: (cmd) => `Whether your checks catch a real defect has not been tested yet: ${cmd} plants one in a copy of the project and shows (a minute or two, your project files are not changed).`,
|
|
121
|
+
todoRest: (n) => `The other entries that apply here (${n}):`,
|
|
122
|
+
todoRestHow: (self) => `install any: ${self} add <name> · what it catches and why: ${self} why <name>`,
|
|
112
123
|
startWith: "Start with these three — born from a real failure, and each closes with one ready command:",
|
|
113
124
|
startCmd: (cmd) => `one line, no kit needed: ${cmd}`,
|
|
114
125
|
startTool: (url) => `the tool: ${url}`,
|
|
@@ -199,6 +210,7 @@ export const en = {
|
|
|
199
210
|
has_docker: ["no Dockerfile or compose", "docker is already here"],
|
|
200
211
|
has_deps: ["no dependency file in sight", "dependencies are declared"],
|
|
201
212
|
has_tests: ["no tests in sight", "tests exist"],
|
|
213
|
+
has_biome: ["the linter is not Biome", "the project's linter is Biome"],
|
|
202
214
|
has_env: ["no environment file", "an environment file exists"],
|
|
203
215
|
has_agent_config: ["the agent was never configured here", "agent settings exist"],
|
|
204
216
|
has_agent_entry: ["no entry point for an agent here", "an entry point for an agent exists"],
|
package/tool/i18n/ru-docs.mjs
CHANGED
|
@@ -68,7 +68,7 @@ const ruDocs = {
|
|
|
68
68
|
runClean: (when) => `Последний прогон ${when} — красных нет.`,
|
|
69
69
|
runRed: (when, names) => `Последний прогон ${when} — КРАСНЫЕ: ${names}.`,
|
|
70
70
|
andMore: (n) => `и ещё ${n}`,
|
|
71
|
-
skipped: (n) => `Не запускались: ${n} — инструмента нет на этой
|
|
71
|
+
skipped: (n) => `Не запускались: ${n} — пропущены по --only/--skip или инструмента нет на этой машине; их состояние неизвестно.`,
|
|
72
72
|
probeNever: "Проба покрытия не делалась — что здесь не прикрыто ничем, НЕИЗВЕСТНО. Это не «прикрыто»: `aqk probe`.",
|
|
73
73
|
probeOff: "Проба покрытия выключена в манифесте (`probe: 0`) — что здесь не прикрыто ничем, никто не считает.",
|
|
74
74
|
probeUnknown: "Пробу покрытия провести не удалось — что здесь не прикрыто ничем, НЕИЗВЕСТНО. Это не «прикрыто».",
|
|
@@ -81,6 +81,28 @@ const ruDocs = {
|
|
|
81
81
|
(behind ? ` (отстала на ${behind} коммитов)` : "") + ".",
|
|
82
82
|
ratchets: (list) => `Храповики: ${list}. Список может только укорачиваться, увеличивать нельзя.`,
|
|
83
83
|
where: (entry) => `Свод правил: ${entry}. Чем доказан диф: \`aqk report --since main\`.`,
|
|
84
|
+
nextTitle: "Дальше, по порядку — вычислено из этого репозитория, а не общий совет:",
|
|
85
|
+
// По виду шага — таблица, а не лесенка тернарников: её не держат в голове, и наш же
|
|
86
|
+
// complexity-limit её поймал.
|
|
87
|
+
nextStep: {
|
|
88
|
+
init: () => "Заведи стандарт: `aqk init` — без .aqk.yml `aqk add` откажет.",
|
|
89
|
+
adopt: (s) => `Объяви проверки, которые у проекта уже есть: в .aqk.yml, в gates: ${s.gates.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Затем \`aqk doctor --run\`.`,
|
|
90
|
+
blind: (s) => `Брак «${s.slug}», подсаженный пробой в ${s.file}, ваши проверки НЕ поймали.` +
|
|
91
|
+
(s.command ? ` Поймать сейчас: \`${s.command}\`.` : "") + ` Держать всегда: \`aqk add ${s.slug}\`.`,
|
|
92
|
+
start: (s) => `Поставь ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (одной строкой, без комплекта: \`${s.command}\`)` : "") +
|
|
93
|
+
", затем `aqk prove` — гейт обязан покраснеть на своём красном образце.",
|
|
94
|
+
},
|
|
95
|
+
nextMore: (n) => `И ещё ${n} — весь список: \`aqk doctor\`.`,
|
|
96
|
+
whenTitle: "Когда что делать:",
|
|
97
|
+
whenCommit: (hook) => hook === true
|
|
98
|
+
? "Перед коммитом → хук `.git/hooks/pre-commit` прогонит проверки сам; не обходи его (`--no-verify`)."
|
|
99
|
+
: "Перед коммитом → `aqk doctor --run --since main`: хука перед коммитом нет, само это не случится.",
|
|
100
|
+
whenRules: [
|
|
101
|
+
"Добавил или поменял проверку → `aqk prove`: гейт обязан покраснеть на своём красном образце, иначе он не проверяет ничего.",
|
|
102
|
+
"Пишешь правило в свод → рядом метка арбитра `<!-- aqk: имя-гейта -->`; гейта нет — `<!-- aqk: человек -->`, и это признание, а не проверка.",
|
|
103
|
+
"Проверка мешает → не ослабляй её (`|| true`, `--exit-zero`, подавление без кода правила): остановись и спроси владельца.",
|
|
104
|
+
"Считаешь, что готово → `aqk report --since main`: каждый изменённый файл должен быть назван проверкой.",
|
|
105
|
+
],
|
|
84
106
|
mapTitle: "ЧТО УМЕЕТ ЭТОТ ИНСТРУМЕНТ. Полный список команд — не пересказ, а тот же список,\nиз которого собрана справка:",
|
|
85
107
|
rulesTitle: (e) => `СВОД ПРАВИЛ ЭТОГО ПРОЕКТА (${e}) — дословно, целиком. Это не приглашение\nпрочитать: он уже здесь.`,
|
|
86
108
|
hookAlready: (p) => `хук уже стоит в ${p} — ничего не меняю.`,
|
|
@@ -198,6 +220,7 @@ const ruDocs = {
|
|
|
198
220
|
},
|
|
199
221
|
],
|
|
200
222
|
report: {
|
|
223
|
+
skippedBySelect: "не запускался (по --only/--skip), состояние неизвестно",
|
|
201
224
|
title: "aqk doctor --run",
|
|
202
225
|
version: "версия",
|
|
203
226
|
level: "уровень",
|
package/tool/i18n/ru-gates.mjs
CHANGED
|
@@ -160,6 +160,7 @@ export const ruGates = {
|
|
|
160
160
|
"методички в .aqk/docs/ остаются на русском — решение, а не недоделка.",
|
|
161
161
|
created: (n) => `создано (${n}):`,
|
|
162
162
|
andMore: (n) => `… и ещё ${n}`,
|
|
163
|
+
ignored: (list) => `служебные файлы комплекта дописаны в .gitignore — это состояние машины, в git ему не место: ${list}`,
|
|
163
164
|
kept: (n) => `уже были на месте, не тронуты (${n}):`,
|
|
164
165
|
overwrite: (cmd) => `перезаписать: ${cmd}`,
|
|
165
166
|
nextTitle: "Что дальше — по порядку:",
|
|
@@ -257,16 +258,20 @@ export const ruGates = {
|
|
|
257
258
|
badEvery: (v) => `в манифесте probe: «${v}» — это не число коммитов. Проба НЕ делается: подставить умолчание значило бы делать не то, что написано.`,
|
|
258
259
|
autoFirst: "пробы покрытия здесь ещё не делали — делаю её сам. Выключить: AQK_PROBE=0",
|
|
259
260
|
auto: (n) => `прошло ${n} коммитов с прошлой пробы — делаю её сам. Выключить: AQK_PROBE=0`,
|
|
261
|
+
autoNotInCi: (cmd) => `в конвейере проба сама не запускается — это минуты сюрпризом в быстрой проверке; поставьте её отдельной задачей: ${cmd} (или AQK_PROBE=1)`,
|
|
260
262
|
title: "aqk probe — чего объявленные проверки не видят",
|
|
261
263
|
method: (files, entries, gates) =>
|
|
262
264
|
`способ: красный образец записи каталога подсаживается в КОПИЮ проекта, и там гоняются ` +
|
|
263
265
|
`ОБЪЯВЛЕННЫЕ гейты — команда берётся как написана, в неё ничего не подставляется. ` +
|
|
264
266
|
`Файлов: ${files}, применимых записей: ${entries}, гейтов зелёных на чистом дереве: ${gates}. ` +
|
|
265
|
-
|
|
267
|
+
`Файлы проекта не меняются; итог пробы пишется в .aqk/last-probe.md (он в .gitignore после init).`,
|
|
266
268
|
fixes: (n) => `починок в истории: ${n}`,
|
|
267
269
|
caught: (names) => `ловит: ${names}`,
|
|
268
270
|
blind: "НЕ ЛОВИТ НИКТО",
|
|
269
271
|
unknown: "проверить нечем — гейт не состоялся (нет делегированной программы)",
|
|
272
|
+
nameless: "не назвал подсаженный файл",
|
|
273
|
+
planting: "краснеет и на зелёном образце — падает от самой подсадки",
|
|
274
|
+
unattributed: (g) => `покраснело: ${g} — поимка не доказана`,
|
|
270
275
|
blindWhere: (file, fixes) => `где: ${file} — починок в истории: ${fixes}`,
|
|
271
276
|
blindWhat: "что подсадили в ваш файл:",
|
|
272
277
|
blindFix: (cmd) => `поймать прямо сейчас, без комплекта: ${cmd}`,
|
package/tool/i18n/ru.mjs
CHANGED
|
@@ -45,6 +45,7 @@ export const ru = {
|
|
|
45
45
|
blob: "собрать методички в один файл GOD_AI.md",
|
|
46
46
|
learn: "кандидаты в правила из локальной переписки: сказано вслух и не записано",
|
|
47
47
|
context: "состояние проекта одним блоком — для контекста агента, а не для чтения",
|
|
48
|
+
vitals: "подключено ли то, чем комплект работает: инструменты гейтов, хуки, свежесть версии",
|
|
48
49
|
contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
|
|
49
50
|
report: "обязательная форма отчёта: что стоит, что нет, что не прочитано; --since <ссылка> — ещё и чем доказан диф",
|
|
50
51
|
badge: "значок уровня для README — и проверка, что он не врёт",
|
|
@@ -66,6 +67,14 @@ export const ru = {
|
|
|
66
67
|
gitignore: "гигиена репозитория",
|
|
67
68
|
git: "проект под контролем версий",
|
|
68
69
|
|
|
70
|
+
runtimeTracked: (f, cmd) => `${f} отслеживается git — каждый прогон его переписывает, и в дереве вечно висит изменённый файл. Вынуть: ${cmd}`,
|
|
71
|
+
runtimeNotIgnored: (f, cmd) => `${f} — состояние этой машины, а git его видит: одно \`git add .\`, и он в коммите. Спрятать: ${cmd}`,
|
|
72
|
+
layoutAdvice: "нет — это совет, прогон не роняет",
|
|
73
|
+
coversImpossible: (entry, gate, linter) => `заявка «${gate} держит ${entry}» неверна: у ${linter} нет правила под этот класс — закрывать его нечем`,
|
|
74
|
+
coversCantCheck: (entry, gate) => `заявку «${gate} держит ${entry}» проверить не умею: линтер гейта не распознан или правил записи для него нет — принято на слово`,
|
|
75
|
+
selectUnknown: (names, groups) => `--only/--skip: не знаю «${names}» — это не гейт из gates: и не группа из groups:${groups ? ` (группы: ${groups})` : ""}. Прогон всего подряд вместо пропуска был бы неправдой, поэтому стоп.`,
|
|
76
|
+
selectSkipped: (names) => `не запускались (по --only/--skip): ${names} — их состояние неизвестно, это не «зелёные»`,
|
|
77
|
+
jobsBad: (v) => `--jobs ждёт целое число от 1: «${v}» не подходит. Прогон по одному под видом параллельного был бы неправдой, поэтому стоп.`,
|
|
69
78
|
rulesByHuman: (total, machine, human) =>
|
|
70
79
|
`правил в точке входа: ${total}. Сторож — ЧЕЛОВЕК у ${human}, машина у ${machine}.`,
|
|
71
80
|
rulesByHumanWhy: "Правило со сторожем-человеком не сторожит никто в тот день, когда человек занят. Это та самая дыра, ради которой комплект и написан, — и напоминание про неё нужно человеку, а не только агенту. Считаны правила ТОЧКИ ВХОДА: обещание, живущее в любом другом файле, не проверяет вообще ничто, и комплект даже не скажет вам, что оно есть.",
|
|
@@ -109,7 +118,9 @@ export const ru = {
|
|
|
109
118
|
blindRan: (g) => `гейт ${g} стоит и прогонялся — а брак в этом файле пропустил`,
|
|
110
119
|
blindInstalled: "объявлено, но проба этот гейт не гоняла (поставлен позже или медленный) — поймает ли, покажет следующая",
|
|
111
120
|
blindMore: (cmd) => `что именно подсадили и куда — ${cmd}`,
|
|
112
|
-
probeNever: (cmd) => `Ловят ли ваши проверки настоящий брак, ещё не проверялось: ${cmd} подсадит его в копию проекта и покажет (минута-две,
|
|
121
|
+
probeNever: (cmd) => `Ловят ли ваши проверки настоящий брак, ещё не проверялось: ${cmd} подсадит его в копию проекта и покажет (минута-две, файлы проекта не меняет).`,
|
|
122
|
+
todoRest: (n) => `Остальные записи, применимые у вас (${n}):`,
|
|
123
|
+
todoRestHow: (self) => `поставить любую: ${self} add <имя> · что она ловит и зачем: ${self} why <имя>`,
|
|
113
124
|
startWith: "Начните с этих трёх — они родились из настоящего отказа и закрываются одной готовой командой:",
|
|
114
125
|
startCmd: (cmd) => `одной строкой, без комплекта: ${cmd}`,
|
|
115
126
|
startTool: (url) => `инструмент: ${url}`,
|
|
@@ -205,6 +216,7 @@ export const ru = {
|
|
|
205
216
|
has_docker: ["нет Dockerfile или compose", "docker уже есть"],
|
|
206
217
|
has_deps: ["не видно файла зависимостей", "зависимости объявлены"],
|
|
207
218
|
has_tests: ["не видно тестов", "тесты есть"],
|
|
219
|
+
has_biome: ["линтер не Biome", "линтер проекта — Biome"],
|
|
208
220
|
has_env: ["нет файла окружения", "файл окружения есть"],
|
|
209
221
|
has_agent_config: ["агента здесь не настраивали", "настройки агента есть"],
|
|
210
222
|
has_agent_entry: ["свода для агента здесь нет", "свод для агента есть"],
|
package/tool/lib/adopt.mjs
CHANGED
|
@@ -95,4 +95,18 @@ function proposeGates(files = {}) {
|
|
|
95
95
|
const ADOPT_FILES = ["package.json", "Makefile", ".pre-commit-config.yaml", "tox.ini", "pyproject.toml"];
|
|
96
96
|
const ADOPT_SCRIPTS = ["scripts/test", "scripts/check", "scripts/lint"];
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
// Чтение с диска — отдельно от разбора: разбор проверяется перебором случаев, чтение — прогоном.
|
|
99
|
+
async function readAdoptFiles(cwd) {
|
|
100
|
+
const { readFile, access } = await import("node:fs/promises");
|
|
101
|
+
const { join } = await import("node:path");
|
|
102
|
+
const files = {};
|
|
103
|
+
for (const n of ADOPT_FILES) {
|
|
104
|
+
try { files[n] = await readFile(join(cwd, n), "utf8"); } catch { /* нет — и ладно */ }
|
|
105
|
+
}
|
|
106
|
+
for (const n of ADOPT_SCRIPTS) {
|
|
107
|
+
try { await access(join(cwd, n)); files[n] = ""; } catch { /* нет — и ладно */ }
|
|
108
|
+
}
|
|
109
|
+
return files;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { proposeGates, readAdoptFiles };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// tool/lib/advice.mjs — ЧТО СОВЕТОВАТЬ ПРОЕКТУ: какие записи каталога его касаются, с каких
|
|
2
|
+
// начать и какой командой закрыть класс брака прямо сейчас, без комплекта.
|
|
3
|
+
//
|
|
4
|
+
// ОТДЕЛЬНЫМ ФАЙЛОМ, а не в repo.mjs: там осмотр репозитория (факты, триггеры, рецепты), здесь —
|
|
5
|
+
// решение, что из этого сказать человеку и агенту. Одно знание на `doctor`, `probe` и блок для
|
|
6
|
+
// агента: жило в двух файлах и росло в обоих; шов вскрыл наш же file-size-limit — repo.mjs
|
|
7
|
+
// дорос до 494 строк при пределе 500.
|
|
8
|
+
import { triggerVerdict } from "./repo.mjs";
|
|
9
|
+
|
|
10
|
+
// Корзины каталога для ЭТОГО проекта: держит · к установке · закрыто другим арбитром ·
|
|
11
|
+
// неприменимо. Одна раскладка на `doctor` и на блок для агента: жила внутри `doctor`, и блоку
|
|
12
|
+
// пришлось бы завести вторую — а два счёта одного и того же расходятся первыми.
|
|
13
|
+
function catalogBuckets(catalog, facts, covered = new Map()) {
|
|
14
|
+
const held = [], todo = [], skip = [], byOther = [];
|
|
15
|
+
for (const rec of catalog) {
|
|
16
|
+
const v = triggerVerdict(rec, facts);
|
|
17
|
+
if (!v.applies) skip.push([rec, v.why]);
|
|
18
|
+
else if (facts.gateKeys.includes(rec.slug)) held.push(rec);
|
|
19
|
+
else if (covered.has(rec.slug)) byOther.push([rec, covered.get(rec.slug)]);
|
|
20
|
+
else todo.push(rec);
|
|
21
|
+
}
|
|
22
|
+
return { held, todo, skip, byOther };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// С ЧЕГО НАЧАТЬ: три записи вместо двадцати равнозначных крестов.
|
|
26
|
+
//
|
|
27
|
+
// Двадцать одинаковых требований — это ноль требований: закрывают первое попавшееся или не
|
|
28
|
+
// закрывают ничего. Порядок НЕ по нашему вкусу; два признака, оба — факты, которые у нас уже
|
|
29
|
+
// есть:
|
|
30
|
+
// · запись родилась из настоящего отказа (`lifecycle: stable` — `proof` ссылается на журнал
|
|
31
|
+
// шишек), то есть она про боль, которая СЛУЧАЛАСЬ, а не про «хорошую практику»;
|
|
32
|
+
// · её можно закрыть одной готовой командой — цена входа минутная.
|
|
33
|
+
// Сначала то, что и больно, и дёшево.
|
|
34
|
+
//
|
|
35
|
+
// При равенстве признаков — по имени: одинаковый ввод обязан давать одинаковый ответ, иначе
|
|
36
|
+
// человек видит разный совет на двух прогонах подряд и перестаёт верить обоим.
|
|
37
|
+
function startWith(entries, facts, n = 3) {
|
|
38
|
+
const langs = facts?.langs ? [...facts.langs] : [];
|
|
39
|
+
const cheap = (e) => {
|
|
40
|
+
const r = e?.recipes && typeof e.recipes === "object" ? e.recipes : {};
|
|
41
|
+
return [...langs, "native"].some((k) => r[k] && !/\{gate\}/.test(r[k])) ? 1 : 0;
|
|
42
|
+
};
|
|
43
|
+
// Зрелость НЕ поле записи, а вычисляемый признак: `proof` ссылается на журнал шишек. Тот же
|
|
44
|
+
// признак, которым каталог отделяет условную запись с первого дня (`entryLifecycle`).
|
|
45
|
+
// Заводить второй счёт нельзя: разъехавшись, они дали бы разные ответы про одну запись.
|
|
46
|
+
const hurt = (e) => (/incidents\//.test(String(e?.proof || "")) ? 1 : 0);
|
|
47
|
+
return [...entries]
|
|
48
|
+
.sort((a, b) =>
|
|
49
|
+
(hurt(b) + cheap(b)) - (hurt(a) + cheap(a)) ||
|
|
50
|
+
cheap(b) - cheap(a) ||
|
|
51
|
+
String(a.slug).localeCompare(String(b.slug)))
|
|
52
|
+
.slice(0, n);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Адрес Biome сверен по реестру github 2026-09-11.
|
|
56
|
+
const BIOME_URL = "https://github.com/biomejs/biome";
|
|
57
|
+
|
|
58
|
+
// Совет по НЕПОКРЫТОМУ классу: команда, которую можно вставить прямо сейчас.
|
|
59
|
+
//
|
|
60
|
+
// Проба находит настоящие дыры и печатала про них «close it: aqk add <имя>» — то есть «поставь
|
|
61
|
+
// нашу штуку». Человек, впервые увидевший комплект, закрывает окно. А готовая однострочная
|
|
62
|
+
// команда под его стек У НАС УЖЕ ЛЕЖИТ в `recipes` записи каталога; мы её не показывали.
|
|
63
|
+
//
|
|
64
|
+
// Замер руками на `requests` (самый скачиваемый python-пакет) 2026-09-10: в
|
|
65
|
+
// `src/requests/utils.py` — 75 коммитов-починок; дописана функция с `except Exception: pass`;
|
|
66
|
+
// их собственные `ruff` и `pytest` дали 0 и на чистой копии, и на подсаженной. Строка, которая
|
|
67
|
+
// поймала бы это, лежала в нашем каталоге всё это время.
|
|
68
|
+
//
|
|
69
|
+
// Переносимый рецепт (`any`) в совет НЕ идёт: он зовёт файл из комплекта, и человеку без
|
|
70
|
+
// комплекта вставить его некуда. Нет родного рецепта под стек — команды нет, и это честнее
|
|
71
|
+
// выдуманной.
|
|
72
|
+
function blindAdvice(entry, facts, hot = {}) {
|
|
73
|
+
const recipes = entry?.recipes && typeof entry.recipes === "object" ? entry.recipes : {};
|
|
74
|
+
// `langs` приходит МНОЖЕСТВОМ, а не массивом — `Array.isArray` тихо давал пустой список, и
|
|
75
|
+
// совет не печатался вовсе. Поймано на живом `requests`: langs = Set(1) { python }.
|
|
76
|
+
const langs = facts?.langs ? [...facts.langs] : [];
|
|
77
|
+
// Тот же порядок, что у `pickRecipe`: свой язык → безъязыковой родной → ничего. Переносимый
|
|
78
|
+
// (`any`) сюда не идёт никогда: он зовёт файл из комплекта, и человеку без комплекта вставить
|
|
79
|
+
// его некуда.
|
|
80
|
+
// Проект на Biome — совет на языке Biome, а не eslint (отзыв с живого проекта 2026-09-11:
|
|
81
|
+
// «начни с трёх» советовал завести eslint проекту на Biome). Одно правило разово через `--only`;
|
|
82
|
+
// `--error-on-warnings` обязателен — правило вне рекомендованных Biome ставит на
|
|
83
|
+
// «предупреждение», и без флага команда выходила с нулём, напечатав находку (Biome 2.5.12).
|
|
84
|
+
const br = typeof entry?.biome_rules === "string" ? entry.biome_rules.trim() : "";
|
|
85
|
+
if (facts?.has_biome && br && br !== "none" && langs.some((l) => l === "javascript" || l === "typescript")) {
|
|
86
|
+
const only = br.split(",").map((x) => x.trim()).filter(Boolean).map((r) => `--only=${r}`).join(" ");
|
|
87
|
+
return { command: `npx @biomejs/biome lint --error-on-warnings ${only} .`, tool: BIOME_URL,
|
|
88
|
+
file: hot.file ?? null, fixes: hot.fixes ?? null, slug: entry?.slug ?? null };
|
|
89
|
+
}
|
|
90
|
+
let cmd = null;
|
|
91
|
+
for (const key of [...langs, "native"]) {
|
|
92
|
+
const r = recipes[key];
|
|
93
|
+
if (!r || /\{gate\}/.test(r)) continue;
|
|
94
|
+
cmd = String(r).replace(/\{dir\}/g, ".")
|
|
95
|
+
// Вычистить то, что относится к НАМ, а не к его проекту. Исключение наших красных
|
|
96
|
+
// образцов нужно УСТАНОВЛЕННОМУ гейту — рядом с ним лежат образцы. Человеку, который
|
|
97
|
+
// команду только копирует, этих каталогов не существует, и флаги про них подрывают
|
|
98
|
+
// доверие: инструмент говорит про чужое хозяйство вместо его кода.
|
|
99
|
+
.replace(/\s--ignore-pattern\s+'[^']*gates\/[^']*'/g, "")
|
|
100
|
+
.replace(/\s--ignore-paths=?\s*'[^']*gates\/[^']*'/g, "")
|
|
101
|
+
.replace(/\s+/g, " ")
|
|
102
|
+
.trim();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
// Адрес — того инструмента, которым команда начинается: поле `tool` общее на все языки, и
|
|
106
|
+
// python-проекту показывалась ссылка на eslint. Первые три слова, а не одно: `npx knip`,
|
|
107
|
+
// `python -m vulture`. Не совпало — весь список: лишняя ссылка лучше, чем ни одной.
|
|
108
|
+
const urls = entry?.tool ? String(entry.tool).split(/\s+·\s+/) : [];
|
|
109
|
+
const head = cmd ? cmd.split(" ").slice(0, 3) : [];
|
|
110
|
+
const own = urls.find((u) => head.includes(u.replace(/\/+$/, "").split("/").pop()));
|
|
111
|
+
const tool = own ?? (entry?.tool ? String(entry.tool) : null);
|
|
112
|
+
return { command: cmd, tool, file: hot.file ?? null, fixes: hot.fixes ?? null, slug: entry?.slug ?? null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { catalogBuckets, startWith, blindAdvice };
|
package/tool/lib/brief.mjs
CHANGED
|
@@ -112,7 +112,9 @@ function beginBrief() {
|
|
|
112
112
|
|
|
113
113
|
// Печать краткого итога. Совет — не чаще раза в сутки и с явным способом отказаться: то, что
|
|
114
114
|
// видишь тридцатый раз, перестаёт читаться и пролистывается вместе с настоящими находками рядом.
|
|
115
|
-
// Отметка времени лежит в .aqk
|
|
115
|
+
// Отметка времени лежит в .aqk/ и попадает в .gitignore при `init` (RUNTIME_FILES в core.mjs):
|
|
116
|
+
// это состояние машины, а не проекта. Раньше здесь было написано «.aqk/ в .gitignore» — а
|
|
117
|
+
// `init` туда ничего не клал, и на живом проекте служебный файл уехал в коммит.
|
|
116
118
|
async function finishBrief(buf, state, todoRecs, ok) {
|
|
117
119
|
if (!buf) return;
|
|
118
120
|
buf.restore();
|
package/tool/lib/cadence.mjs
CHANGED
|
@@ -89,4 +89,14 @@ function parseRan(text) {
|
|
|
89
89
|
return m ? new Set(m[1].split(/\s+/).filter(Boolean)) : null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// Запускаться ли пробе САМОЙ внутри `doctor --run`. Не в конвейере: там это +2–3 минуты сюрпризом
|
|
93
|
+
// в случайном прогоне (отзыв с живого проекта 2026-09-11), и место пробы — отдельная задача. Не в
|
|
94
|
+
// коротком режиме: там хук на воротах коммита. `AQK_PROBE=0` — выключить, `AQK_PROBE=1` — включить
|
|
95
|
+
// и в конвейере.
|
|
96
|
+
function autoProbeAllowed({ brief = false, env = process.env } = {}) {
|
|
97
|
+
if (env.AQK_PROBE === "0" || brief) return false;
|
|
98
|
+
if (env.AQK_PROBE === "1") return true;
|
|
99
|
+
return !(env.CI || env.GITHUB_ACTIONS || env.GITLAB_CI || env.BUILDKITE || env.JENKINS_URL);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { probeDue, probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan, autoProbeAllowed };
|
package/tool/lib/core.mjs
CHANGED
|
@@ -142,9 +142,47 @@ async function writeIfAbsent(path, content, { force }) {
|
|
|
142
142
|
return true;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// СЛУЖЕБНЫЕ ФАЙЛЫ: состояние ЭТОЙ машины, переписываются каждым прогоном — в git им не место.
|
|
146
|
+
// Один список на `init` (кладёт в .gitignore), `doctor` (предупреждает, если git их видит) и
|
|
147
|
+
// читателей. Отзыв с живого проекта 2026-09-11: `.aqk/last-run.md` однажды закоммитили, и каждый
|
|
148
|
+
// `make check` оставлял изменённый файл. Целиком `.aqk/` не игнорируется: методички и правила в
|
|
149
|
+
// нём — содержимое проекта.
|
|
150
|
+
const RUNTIME_FILES = ["last-run.md", "last-probe.md", "advice-shown", "update-checked"];
|
|
151
|
+
const L_IGNORE_NOTE = LANG === "en"
|
|
152
|
+
? "# aqk: this machine's state — rewritten by every run, it does not belong in git"
|
|
153
|
+
: "# aqk: состояние этой машины — переписывается каждым прогоном, в git ему не место";
|
|
154
|
+
|
|
155
|
+
// Дописать служебные файлы в .gitignore, не трогая чужих строк и не дублируя своих. Возвращает
|
|
156
|
+
// список добавленных строк — `init` называет их вслух: правка чужого файла без слова была бы
|
|
157
|
+
// тем самым «пишу в дерево без спроса».
|
|
158
|
+
async function ensureIgnored(cwd = CWD) {
|
|
159
|
+
const { readFile, writeFile } = await import("node:fs/promises");
|
|
160
|
+
const file = join(cwd, ".gitignore");
|
|
161
|
+
let text = "";
|
|
162
|
+
try { text = await readFile(file, "utf8"); } catch { /* файла нет — создадим */ }
|
|
163
|
+
const have = new Set(text.split(/\r?\n/).map((l) => l.trim()));
|
|
164
|
+
const add = RUNTIME_FILES.map((f) => `${TARGET_DIR}/${f}`).filter((l) => !have.has(l) && !have.has(`/${l}`));
|
|
165
|
+
if (!add.length) return [];
|
|
166
|
+
const head = text && !text.endsWith("\n") ? "\n" : "";
|
|
167
|
+
await writeFile(file, `${text}${head}${text ? "\n" : ""}${L_IGNORE_NOTE}\n${add.join("\n")}\n`, "utf8");
|
|
168
|
+
return add;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Стоит ли хук pre-commit НА САМОМ ДЕЛЕ — в `.git/hooks`, а не в `.pre-commit-config.yaml`:
|
|
172
|
+
// запись в конфиге — намерение, сработает только то, что лежит в гите. Три ответа: true — стоит,
|
|
173
|
+
// false — нет, null — не git или файл не прочитать («не знаем» не сливается с «нет»).
|
|
174
|
+
// Одна функция на `vitals` (подключено ли) и `context` (что сказать агенту перед коммитом).
|
|
175
|
+
async function preCommitHook(cwd = CWD) {
|
|
176
|
+
const { readFile } = await import("node:fs/promises");
|
|
177
|
+
if (!(await exists(join(cwd, ".git")))) return null;
|
|
178
|
+
const hook = join(cwd, ".git", "hooks", "pre-commit");
|
|
179
|
+
if (!(await exists(hook))) return false;
|
|
180
|
+
try { return /pre-commit|aqk/i.test(await readFile(hook, "utf8")); } catch { return null; }
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
export {
|
|
146
184
|
copyDir, writeIfAbsent,
|
|
147
185
|
PKG_ROOT, CWD, DOCS_SRC, RULES_SRC, TARGET_DIR, docPath,
|
|
148
186
|
MANIFEST, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB,
|
|
149
|
-
SELF, REPO_URL, c, exists, die, FEEDBACK_MARK, commandRows,
|
|
187
|
+
SELF, REPO_URL, c, exists, die, FEEDBACK_MARK, commandRows, preCommitHook, RUNTIME_FILES, ensureIgnored,
|
|
150
188
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// tool/lib/gate-worker.mjs — рабочий поток параллельного прогона (`doctor --run --jobs N`).
|
|
2
|
+
//
|
|
3
|
+
// ПОЧЕМУ ПОТОК, А НЕ АСИНХРОННЫЙ ЗАПУСК. Прогон стоит на `spawnSync` с таймаутом: его поведение
|
|
4
|
+
// на зависшем гейте замерено и описано (execution.mjs). Асинхронный `spawn` потребовал бы своего
|
|
5
|
+
// убийства дерева процессов на таймауте — на Windows это отдельная история. Поток выполняет ТОТ ЖЕ
|
|
6
|
+
// `spawnSync` с тем же таймаутом, и семантика прогона не меняется ни в чём, кроме одновременности.
|
|
7
|
+
// Зависимостей это не добавляет: worker_threads встроены в Node.
|
|
8
|
+
import { parentPort } from "node:worker_threads";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { gateCommand } from "./execution.mjs";
|
|
11
|
+
|
|
12
|
+
parentPort.on("message", ({ id, cmd, cwd, timeout }) => {
|
|
13
|
+
const r = spawnSync(gateCommand(cmd), { shell: true, cwd, encoding: "utf8", timeout });
|
|
14
|
+
parentPort.postMessage({
|
|
15
|
+
id, status: r.status, stdout: r.stdout || "", stderr: r.stderr || "",
|
|
16
|
+
error: r.error ? { code: r.error.code || String(r.error.message || r.error) } : null,
|
|
17
|
+
});
|
|
18
|
+
});
|
package/tool/lib/history.mjs
CHANGED
|
@@ -117,9 +117,36 @@ function probeSummary({ caught = 0, blind = 0, unknown = 0, unprobed = 0 } = {})
|
|
|
117
117
|
// Найдено пробой на самом комплекте 2026-09-11: образец лёг на место общей библиотеки
|
|
118
118
|
// kit/gates/_skip.sh, двенадцать гейтов вышли с кодом 2 — включая тот, что этот образец в
|
|
119
119
|
// отдельной папке ловит. Остальные молчали, и проба назвала класс слепым.
|
|
120
|
+
// «ПОКРАСНЕЛ» ≠ «ПОЙМАЛ». Отзыв с живого проекта 2026-09-11: класс «цвет из токена темы» отмечен
|
|
121
|
+
// пойманным линтером, хотя Biome цвета не проверяет, — подсаженный кусок сломал форматирование.
|
|
122
|
+
// Вердикт сравнивал коды возврата и выбрасывал вывод. Теперь гейт засчитывается, только если в
|
|
123
|
+
// его выводе ПОСЛЕ подсадки подсаженный файл назван чаще, чем до неё. Путь ищется в любой
|
|
124
|
+
// форме: `src/x`, `./src/x`, абсолютный из песочницы (подстрока), с обратными слешами Windows.
|
|
125
|
+
// Одно имя файла без каталога не засчитывается: у двух файлов оно бывает одинаковым, и
|
|
126
|
+
// поимка чужого файла выдалась бы за поимку нашего. Безымянное падение — «неизвестно».
|
|
127
|
+
function namesPlant(before, after, rel) {
|
|
128
|
+
const forms = [rel, String(rel).replace(/\//g, "\\")];
|
|
129
|
+
const count = (s) => forms.reduce((n, f) => n + String(s || "").split(f).length - 1, 0);
|
|
130
|
+
return count(after) > count(before);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ПАРА, А НЕ ОДИН ОБРАЗЕЦ. Имени файла мало: Biome, падая на форматировании, тоже называет файл.
|
|
134
|
+
// Поэтому поимка подтверждается зелёным образцом той же записи, положенным в то же место, — тем
|
|
135
|
+
// же приёмом, каким мы требуем от чужого гейта доказательства. Три исхода:
|
|
136
|
+
// caught — на красном покраснел и назвал файл, на зелёном промолчал (или файла не назвал);
|
|
137
|
+
// nameless — покраснел, но подсаженного файла не назвал: упал по своей причине;
|
|
138
|
+
// planting — краснеет и на зелёном, называя тот же файл: падает от самой подсадки.
|
|
139
|
+
// Зелёного образца под это расширение нет — судим по имени файла: пусть слабее, но это не
|
|
140
|
+
// «поймано» на ровном месте, и в README записи такой случай назван.
|
|
141
|
+
function catchVerdict(beforeOut, red, green, rel) {
|
|
142
|
+
if (!namesPlant(beforeOut, red?.out, rel)) return "nameless";
|
|
143
|
+
if (green && green.code === 1 && namesPlant(beforeOut, green.out, rel)) return "planting";
|
|
144
|
+
return "caught";
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
function probeVerdictPaired(before, after) {
|
|
121
148
|
const byName = new Map(after.map((r) => [r.name, r]));
|
|
122
|
-
let usable = 0, alreadyRed = 0, failed = 0, caught = 0, brokenByPlant = 0;
|
|
149
|
+
let usable = 0, alreadyRed = 0, failed = 0, caught = 0, brokenByPlant = 0, unattributed = 0;
|
|
123
150
|
for (const b of before) {
|
|
124
151
|
const a = byName.get(b.name);
|
|
125
152
|
const broke = (r) => !r || (r.code !== 0 && r.code !== 1);
|
|
@@ -130,10 +157,12 @@ function probeVerdictPaired(before, after) {
|
|
|
130
157
|
}
|
|
131
158
|
if (b.code === 1) { alreadyRed++; continue; }
|
|
132
159
|
usable++;
|
|
133
|
-
|
|
160
|
+
// `named === false` — покраснел, но подсаженного файла не назвал. `undefined` — вызывающий
|
|
161
|
+
// вывода не собирал (старые вызовы): тогда прежнее правило.
|
|
162
|
+
if (a.code === 1) { if (a.named === false) unattributed++; else caught++; }
|
|
134
163
|
}
|
|
135
|
-
const verdict = caught ? "caught" : !usable || brokenByPlant ? "unknown" : "blind";
|
|
136
|
-
return { verdict, usable, alreadyRed, failed, caught, brokenByPlant };
|
|
164
|
+
const verdict = caught ? "caught" : !usable || brokenByPlant || unattributed ? "unknown" : "blind";
|
|
165
|
+
return { verdict, usable, alreadyRed, failed, caught, brokenByPlant, unattributed };
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
// Счёт непокрытого КЛАССАМИ, а не пробами. Замер на десяти живых репозиториях 2026-09-10:
|
|
@@ -160,4 +189,4 @@ function countProbe(records) {
|
|
|
160
189
|
};
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
export { isFix, fixHotspots, probeSummary, probeVerdictPaired, countProbe };
|
|
192
|
+
export { isFix, fixHotspots, probeSummary, probeVerdictPaired, countProbe, namesPlant, catchVerdict };
|