agent-quality-kit 0.4.2 → 0.6.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 +136 -12
- package/README.ru.md +119 -11
- package/kit/docs/ready-made-rules.md +40 -0
- package/kit/gates/README.md +35 -0
- package/kit/gates/_native.sh +18 -2
- package/kit/gates/_skip.sh +18 -0
- package/kit/gates/ci-actually-fails/README.md +42 -0
- package/kit/gates/ci-actually-fails/check.sh +93 -0
- package/kit/gates/ci-actually-fails/gate.yml +14 -0
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +14 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
- package/kit/gates/color-from-token/README.md +52 -0
- package/kit/gates/color-from-token/check.sh +69 -0
- package/kit/gates/color-from-token/gate.yml +15 -0
- package/kit/gates/color-from-token/green/Button.tsx +4 -0
- package/kit/gates/color-from-token/green/Panel.vue +4 -0
- package/kit/gates/color-from-token/green/card.css +5 -0
- package/kit/gates/color-from-token/green/notes.md +2 -0
- package/kit/gates/color-from-token/green/tokens.css +7 -0
- package/kit/gates/color-from-token/red/Button.tsx +4 -0
- package/kit/gates/color-from-token/red/Panel.vue +4 -0
- package/kit/gates/color-from-token/red/card.css +5 -0
- package/kit/gates/commit-explains-itself/check.sh +25 -2
- package/kit/gates/duplicate-code/check.sh +13 -4
- package/kit/gates/duplicate-code/gate.yml +11 -2
- package/kit/gates/gate-has-samples/check.sh +9 -3
- package/kit/gates/gate-not-weakened/README.md +54 -0
- package/kit/gates/gate-not-weakened/check.sh +72 -0
- package/kit/gates/gate-not-weakened/gate.yml +15 -0
- package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
- package/kit/gates/gate-not-weakened/green/payments.py +6 -0
- package/kit/gates/gate-not-weakened/green/release.sh +2 -0
- package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
- package/kit/gates/gate-not-weakened/red/payments.py +6 -0
- package/kit/gates/gate-not-weakened/red/release.sh +2 -0
- package/kit/gates/gates-are-runnable/check.sh +7 -1
- package/kit/gates/gates-run-in-ci/check.sh +7 -1
- package/kit/gates/lesson-has-outcome/check.sh +6 -1
- package/kit/gates/promise-has-gate/README.md +50 -0
- package/kit/gates/promise-has-gate/check.sh +88 -0
- package/kit/gates/promise-has-gate/gate.yml +14 -0
- package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
- package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
- package/kit/gates/test-has-assertion/README.md +47 -0
- package/kit/gates/test-has-assertion/check.sh +194 -0
- package/kit/gates/test-has-assertion/gate.yml +15 -0
- package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
- package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
- package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
- package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
- package/kit/ratchet/ratchet.sh +9 -2
- package/kit/rules/general.md +14 -0
- package/llms.txt +58 -0
- package/package.json +4 -2
- package/tool/commands/doctor.mjs +106 -11
- package/tool/commands/gates.mjs +18 -3
- package/tool/commands/project.mjs +13 -4
- package/tool/commands/report.mjs +2 -2
- package/tool/i18n/en.mjs +55 -0
- package/tool/i18n/ru.mjs +61 -0
- package/tool/i18n/templates-en.mjs +9 -9
- package/tool/i18n/templates-ru.mjs +9 -9
- package/tool/lib/baseline.mjs +87 -0
- package/tool/lib/core.mjs +10 -2
- package/tool/lib/manifest.mjs +69 -2
- package/tool/lib/repo.mjs +7 -0
- package/tool/lib/scope.mjs +96 -0
- package/tool/program.mjs +1 -0
- package/tool/selfcheck/gates.sh +29 -5
- package/tool/selfcheck/lifecycle.mjs +29 -0
- package/tool/selfcheck/mutation.sh +95 -0
- package/tool/selfcheck/smoke.sh +269 -8
- package/tool/selfcheck/syntax.sh +9 -1
- package/tool/selfcheck/units.mjs +160 -1
package/kit/ratchet/ratchet.sh
CHANGED
|
@@ -17,7 +17,12 @@ REG="${1:-}"; shift || true
|
|
|
17
17
|
|
|
18
18
|
# Ключ нарушения обязан переживать правку соседних строк, иначе сдвиг на строку читается
|
|
19
19
|
# как новое нарушение. Поэтому номер строки из ключа убирается.
|
|
20
|
-
|
|
20
|
+
#
|
|
21
|
+
# Два шаблона, а не один: у гейта duplicate-code строка несёт ДВА места — «a:171 и b:188: …»,
|
|
22
|
+
# и за номером первого идёт не двоеточие, а пробел. Шаблон с двоеточием убирал номер только
|
|
23
|
+
# у второго файла пары, и сдвиг кода в первом читался как новое нарушение — ровно то, от чего
|
|
24
|
+
# храповик защищает.
|
|
25
|
+
keys() { grep -E '^[^[:space:]].*:' | sed -E 's/:[0-9]+:/:/g; s/:[0-9]+( |$)/:\1/g' | LC_ALL=C sort -u; }
|
|
21
26
|
|
|
22
27
|
OUT="$("$@" 2>&1)"
|
|
23
28
|
NOW="$(printf '%s\n' "$OUT" | keys)"
|
|
@@ -26,7 +31,9 @@ if [ ! -f "$REG" ]; then
|
|
|
26
31
|
echo "нет реестра $REG — сначала: aqk ratchet <гейт>"
|
|
27
32
|
exit 2
|
|
28
33
|
fi
|
|
29
|
-
|
|
34
|
+
# Та же сортировка, что в keys(): comm сравнивает построчно и требует, чтобы оба входа были
|
|
35
|
+
# упорядочены одинаково. Разная локаль у двух сторон — это тихо разъехавшееся сравнение.
|
|
36
|
+
WAS="$(grep -vE '^\s*(#|$)' "$REG" | LC_ALL=C sort -u)"
|
|
30
37
|
|
|
31
38
|
NEW="$(comm -23 <(printf '%s\n' "$NOW") <(printf '%s\n' "$WAS"))"
|
|
32
39
|
GONE="$(comm -13 <(printf '%s\n' "$NOW") <(printf '%s\n' "$WAS"))"
|
package/kit/rules/general.md
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
- **Ни одного тихого отказа.** Ошибка обработана и записана либо проброшена.
|
|
8
8
|
- **Границы явные.** На стыках — проверка входа, а не доверие.
|
|
9
9
|
|
|
10
|
+
## Сомнение — повод посмотреть наружу
|
|
11
|
+
|
|
12
|
+
Агент отвечает уверенно всегда: и когда знает, и когда достраивает по памяти. Со стороны это
|
|
13
|
+
неотличимо, а цена разная. Поэтому четыре случая обязаны кончаться поиском, а не догадкой:
|
|
14
|
+
|
|
15
|
+
| Случай | Что происходит без поиска |
|
|
16
|
+
|---|---|
|
|
17
|
+
| не знаешь, как принято **сейчас** | пишется то, что было принято на момент обучения |
|
|
18
|
+
| не знаешь, есть ли **готовое** | пишется свой велосипед, который потом чинить самому |
|
|
19
|
+
| собираешься написать распространённую вещь | половина работы уже сделана кем-то и проверена |
|
|
20
|
+
| помнишь ответ, но **из обучения, а не из проверки** | вспомненный API мог быть переименован или убран |
|
|
21
|
+
|
|
22
|
+
Правило дешевле, чем кажется: поиск стоит минуту, а неверная догадка — правку, ревью и шишку.
|
|
23
|
+
|
|
10
24
|
## Запрещено в готовом коде
|
|
11
25
|
|
|
12
26
|
- отладочная печать;
|
package/llms.txt
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# AQK — Agent Quality Kit
|
|
2
|
+
|
|
3
|
+
> A standard and a CLI that check whether a repository is ready to have its code written by AI
|
|
4
|
+
> coding agents. Every rule the project promises to follow becomes a command with an exit code,
|
|
5
|
+
> so a machine holds the promise instead of somebody's attention. Reports a level from AQK-0 to
|
|
6
|
+
> AQK-3, computed by a run — never by a questionnaire and never by a model's opinion.
|
|
7
|
+
|
|
8
|
+
Vendor-neutral: works with any coding agent (Claude Code, Codex, Cursor, Gemini CLI, GitHub
|
|
9
|
+
Copilot, Windsurf, Aider, OpenCode) and with no AI at all. It calls no vendor API and needs no
|
|
10
|
+
key. Stack-neutral: portable checks are plain `sh`; where the project already has a native tool
|
|
11
|
+
(ruff, eslint, knip, jscpd) the check uses it instead and says so when it falls back.
|
|
12
|
+
|
|
13
|
+
Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
14
|
+
|
|
15
|
+
## Use it
|
|
16
|
+
|
|
17
|
+
- Check an existing repository: `npx agent-quality-kit doctor` (read-only: writes nothing, sends
|
|
18
|
+
nothing)
|
|
19
|
+
- Start a new one: `npx agent-quality-kit start`
|
|
20
|
+
- Install one guard from the catalogue: `npx agent-quality-kit add <name>`
|
|
21
|
+
- Check the minimum a project needs before agents can be handed the work:
|
|
22
|
+
`npx agent-quality-kit doctor --baseline` (14 of 50 points confirmed by a run, ecosystem-neutral;
|
|
23
|
+
the other 36 are named as a number, not hidden)
|
|
24
|
+
- Fail a pipeline below a level or on a failed gate: `npx agent-quality-kit doctor --run --min 1`
|
|
25
|
+
- Show only what a diff introduced, so a legacy repo is usable from day one: `doctor --run --since main`
|
|
26
|
+
- Exit codes: 0 pass, 1 below the level or a gate failed
|
|
27
|
+
- As a pre-commit hook: `repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit` with
|
|
28
|
+
`id: aqk` (blocking), `aqk-doctor` (read-only) or `aqk-baseline`. pre-commit installs the
|
|
29
|
+
package itself; there are no dependencies to pull in.
|
|
30
|
+
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.2` with `min: 1`
|
|
31
|
+
(https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
32
|
+
|
|
33
|
+
## What makes it different
|
|
34
|
+
|
|
35
|
+
- The level is computed by running what the repository declares — not from a self-assessment
|
|
36
|
+
questionnaire, not from the presence of a config file, not from a language model's judgement.
|
|
37
|
+
- Every guard must ship a red and a green sample, and a machine checks that the guard goes red on
|
|
38
|
+
the red one and stays quiet on the green one. A check that cannot fail is indistinguishable
|
|
39
|
+
from a check that works; this is the only place that requirement is enforced.
|
|
40
|
+
- An entry enters the catalogue only when it names a real failure it caught, recorded in the
|
|
41
|
+
bruise journal.
|
|
42
|
+
|
|
43
|
+
## Files it reads and writes
|
|
44
|
+
|
|
45
|
+
- `AGENTS.md` — what the agent reads first (the entry point; `CLAUDE.md` and others work too)
|
|
46
|
+
- `.aqk.yml` — the manifest: entry, rules, gates as commands, samples, ratchets, lessons
|
|
47
|
+
- `.aqkignore` — paths the scanning checks must not read (brought-in code, vendored, generated)
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
- [README](README.md): what it is, how to install, the catalogue, the badge, CI
|
|
52
|
+
- [SPEC.md](SPEC.md): the standard itself — levels, manifest, how an entry is accepted
|
|
53
|
+
- [Project baseline](kit/docs/ai/project-baseline.md): what a project needs before agents can be
|
|
54
|
+
handed the work, in plain words, independent of language and tooling
|
|
55
|
+
- [The gate catalogue](kit/gates/README.md): what a gate is and the bar an entry must clear
|
|
56
|
+
- [The bruise journal](incidents/README.md): every defect found, and what was made of it
|
|
57
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): how to bring a gate
|
|
58
|
+
- [SECURITY.md](SECURITY.md): reporting a vulnerability, and the trust model of the manifest
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-quality-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Turns the rules an agent is supposed to follow into commands with exit codes, and reports which of them actually run. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"kit",
|
|
12
12
|
"README.md",
|
|
13
13
|
"README.ru.md",
|
|
14
|
+
"llms.txt",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
],
|
|
43
44
|
"knip": {
|
|
44
45
|
"entry": [
|
|
45
|
-
"tool/selfcheck/units.mjs"
|
|
46
|
+
"tool/selfcheck/units.mjs",
|
|
47
|
+
"tool/selfcheck/lifecycle.mjs"
|
|
46
48
|
],
|
|
47
49
|
"project": [
|
|
48
50
|
"tool/**/*.mjs"
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -3,11 +3,48 @@
|
|
|
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 {
|
|
7
|
-
import {
|
|
6
|
+
import { scopeOutput, changedFiles } from "../lib/scope.mjs";
|
|
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
9
|
import { detectFacts, readCatalog, triggerVerdict, recipeFor } from "../lib/repo.mjs";
|
|
10
|
+
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
9
11
|
import { L } from "../i18n/index.mjs";
|
|
10
12
|
|
|
13
|
+
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
14
|
+
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
15
|
+
async function reportBaseline(man, facts) {
|
|
16
|
+
const { readdir, readFile } = await import("node:fs/promises");
|
|
17
|
+
let files = [];
|
|
18
|
+
try {
|
|
19
|
+
files = (await readdir(CWD, { withFileTypes: true })).map((d) => d.name);
|
|
20
|
+
} catch { /* пустой список честнее выдуманного: ни один пункт не подтвердится */ }
|
|
21
|
+
|
|
22
|
+
// Файлы зависимостей читаются целиком и склеиваются: трекер ошибок объявляют по-разному в
|
|
23
|
+
// каждой экосистеме, а искать его надо одинаково.
|
|
24
|
+
let depsText = "";
|
|
25
|
+
for (const f of DEP_FILES) {
|
|
26
|
+
if (!files.some((n) => n.toLowerCase() === f)) continue;
|
|
27
|
+
try { depsText += (await readFile(join(CWD, f), "utf8")).toLowerCase() + "\n"; } catch { /* нечитаемый файл — просто не признак */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rows = assessBaseline({ files, gateKeys: facts.gateKeys, facts, manifest: man || {}, depsText });
|
|
31
|
+
const okCount = rows.filter((r) => r.ok).length;
|
|
32
|
+
|
|
33
|
+
console.log(c.bold(`\n ${L.baseline.heading}\n`));
|
|
34
|
+
console.log(c.dim(` ${L.baseline.intro(rows.length, BASELINE_TOTAL)}`));
|
|
35
|
+
console.log(c.dim(` ${L.baseline.caveat}\n`));
|
|
36
|
+
for (const r of rows) {
|
|
37
|
+
const mark = r.ok ? c.green("✔") : c.yellow("✘");
|
|
38
|
+
const title = L.baseline.titles[r.key] || r.key;
|
|
39
|
+
console.log(` ${mark} ${String(r.n).padStart(2)}. ${title}`);
|
|
40
|
+
console.log(c.dim(` ${r.ok ? L.baseline.by(r.by) : L.baseline.none}`));
|
|
41
|
+
}
|
|
42
|
+
console.log(
|
|
43
|
+
"\n " + (okCount === rows.length ? c.green(`${okCount}/${rows.length}`) : c.yellow(`${okCount}/${rows.length}`)) +
|
|
44
|
+
c.dim(` · ${L.baseline.eyes(BASELINE_TOTAL - rows.length, "kit/docs/ai/project-baseline.md")}\n`)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
11
48
|
async function reportCatalog(man, facts) {
|
|
12
49
|
const catalog = await readCatalog();
|
|
13
50
|
if (!catalog.length) return;
|
|
@@ -58,10 +95,27 @@ function declaredGates(man) {
|
|
|
58
95
|
.filter(([, cmd]) => cmd);
|
|
59
96
|
}
|
|
60
97
|
|
|
61
|
-
|
|
98
|
+
// Ссылка, относительно которой сужается вывод: `--since main`, `--since HEAD~5`.
|
|
99
|
+
// Без значения флаг бессмыслен — молча взять умолчание нельзя: «сужено не тем» неотличимо
|
|
100
|
+
// от «не сужено».
|
|
101
|
+
function sinceRef(argv = process.argv) {
|
|
102
|
+
const i = argv.indexOf("--since");
|
|
103
|
+
if (i === -1) return null;
|
|
104
|
+
const v = argv[i + 1];
|
|
105
|
+
return v && !v.startsWith("-") ? v : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runGates(man, opts = {}) {
|
|
62
109
|
const gates = declaredGates(man);
|
|
63
110
|
if (!gates.length) return { failed: 0, ran: 0, results: [] };
|
|
64
111
|
|
|
112
|
+
// Сужение по дифу — договор с человеком, и он должен видеть, ЧТО именно сужено. Пустой диф
|
|
113
|
+
// называется вслух: иначе «все гейты зелёные» означало бы «сравнили не с тем» и читалось бы
|
|
114
|
+
// как успех. Это тот же класс, что и весь стандарт, только внутри нашего флага.
|
|
115
|
+
const scoped = opts.since ? changedFiles(opts.since, CWD) : null;
|
|
116
|
+
if (opts.since && scoped === null) die(L.doctor.sinceBadRef(opts.since));
|
|
117
|
+
if (scoped) console.log(c.dim(`\n ${L.doctor.sinceHeading(opts.since, scoped.size)}`));
|
|
118
|
+
|
|
65
119
|
console.log(c.bold(`\n ${L.doctor.runHeading}\n`));
|
|
66
120
|
let failed = 0;
|
|
67
121
|
const results = [];
|
|
@@ -82,8 +136,30 @@ function runGates(man) {
|
|
|
82
136
|
console.log(` ${c.green("✔")} ${name.padEnd(14)} ${c.dim(`${secs}s · ${cmd}`)}`);
|
|
83
137
|
results.push({ name, cmd, ok: true, secs });
|
|
84
138
|
} else {
|
|
139
|
+
let out = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
|
|
140
|
+
|
|
141
|
+
// Сужение до дифа. Три исхода, и все три называются вслух.
|
|
142
|
+
if (scoped) {
|
|
143
|
+
const s = scopeOutput(out, scoped);
|
|
144
|
+
if (!s.scopable) {
|
|
145
|
+
// Гейт печатает вердикт без путей — сузить нечем. Признать его успешным значило бы
|
|
146
|
+
// выдать провал за тишину; остаётся красным, и причина названа.
|
|
147
|
+
console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.exitCode(code))} ${c.dim(`· ${L.doctor.notScopable}`)}`);
|
|
148
|
+
failed++;
|
|
149
|
+
results.push({ name, cmd, ok: false, secs, code, note: L.doctor.notScopable });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (s.findings === 0) {
|
|
153
|
+
// Долг есть, но не в том, что внёс диф. Зелёный — но с числом спрятанного: молчаливое
|
|
154
|
+
// «всё хорошо» здесь было бы неправдой.
|
|
155
|
+
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 });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
out = s.kept;
|
|
160
|
+
}
|
|
161
|
+
|
|
85
162
|
failed++;
|
|
86
|
-
const out = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
|
|
87
163
|
console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.exitCode(code))} ${c.dim(`· ${secs}s · ${cmd}`)}`);
|
|
88
164
|
for (const line of out.slice(0, 3)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
89
165
|
if (out.length > 3) console.log(c.dim(` ${L.doctor.moreLines(out.length - 3)}`));
|
|
@@ -158,6 +234,15 @@ async function cmdDoctor() {
|
|
|
158
234
|
}
|
|
159
235
|
|
|
160
236
|
const man = await readManifest();
|
|
237
|
+
|
|
238
|
+
// Опечатка в имени поля означала «поля нет»: вердикт выдавался неверный, а причина молчала.
|
|
239
|
+
// Называем поле и говорим, какие бывают — иначе человек ищет ошибку в проекте, а она в файле.
|
|
240
|
+
const unknown = unknownKeys(man);
|
|
241
|
+
if (unknown.length) {
|
|
242
|
+
console.log(c.yellow(`\n ${L.doctor.manifestUnknown(unknown)}`));
|
|
243
|
+
console.log(c.dim(` ${L.doctor.manifestKnown(KNOWN_KEYS)}\n`));
|
|
244
|
+
}
|
|
245
|
+
|
|
161
246
|
const { reached, steps } = await assessLevel(man);
|
|
162
247
|
|
|
163
248
|
console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
|
|
@@ -190,15 +275,21 @@ async function cmdDoctor() {
|
|
|
190
275
|
}
|
|
191
276
|
|
|
192
277
|
const facts = await detectFacts(man);
|
|
278
|
+
if (process.argv.includes("--baseline")) {
|
|
279
|
+
await reportBaseline(man, facts);
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
193
282
|
await reportCatalog(man, facts);
|
|
194
283
|
|
|
195
284
|
// «Объявлен» ≠ «работает». Без --run говорим это вслух, а не молчим.
|
|
196
285
|
const wantRun = process.argv.includes("--run");
|
|
197
286
|
const gates = declaredGates(man);
|
|
198
287
|
let gateFailed = 0;
|
|
288
|
+
let failedNames = [];
|
|
199
289
|
if (wantRun) {
|
|
200
|
-
const run = runGates(man);
|
|
290
|
+
const run = runGates(man, { since: sinceRef() });
|
|
201
291
|
gateFailed = run.failed;
|
|
292
|
+
failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
|
|
202
293
|
await writeRunReport({ version, reached, results: run.results });
|
|
203
294
|
} else if (gates.length) {
|
|
204
295
|
console.log(
|
|
@@ -211,12 +302,16 @@ async function cmdDoctor() {
|
|
|
211
302
|
const minIdx = process.argv.indexOf("--min");
|
|
212
303
|
const min = minIdx > -1 ? Number(process.argv[minIdx + 1]) : null;
|
|
213
304
|
if (min !== null) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
305
|
+
const levelOk = reached >= min;
|
|
306
|
+
const pass = levelOk && gateFailed === 0;
|
|
307
|
+
// Две разные развилки, и сообщение обязано их различать. «Порог не пройден: сейчас AQK-1»
|
|
308
|
+
// при пороге AQK-1 противоречит само себе и отправляет чинить манифест, когда падал гейт.
|
|
309
|
+
const now = reached < 0 ? L.doctor.levelNone : reached;
|
|
310
|
+
let line;
|
|
311
|
+
if (pass) line = c.green(` ${L.doctor.thresholdPass(min)}\n`);
|
|
312
|
+
else if (!levelOk) line = c.red(` ${L.doctor.thresholdFail(min, now)}\n`);
|
|
313
|
+
else line = c.red(` ${L.doctor.thresholdGateFail(min, now, failedNames)}\n`);
|
|
314
|
+
console.log(line);
|
|
220
315
|
process.exit(pass ? 0 : 1);
|
|
221
316
|
}
|
|
222
317
|
process.exit(missing || reached < 0 || gateFailed ? 1 : 0);
|
package/tool/commands/gates.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
CWD, PKG_ROOT, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB, MANIFEST, SELF, c, exists, die,
|
|
9
9
|
copyDir,
|
|
10
10
|
} from "../lib/core.mjs";
|
|
11
|
-
import { parseManifest, readManifest, manifestWithGate } from "../lib/manifest.mjs";
|
|
11
|
+
import { parseManifest, readManifest, manifestWithGate, entryLifecycle } from "../lib/manifest.mjs";
|
|
12
12
|
import {
|
|
13
13
|
detectFacts, readCatalog, pickRecipe, triggerVerdict, stems, overlap, matchCatalog,
|
|
14
14
|
} from "../lib/repo.mjs";
|
|
@@ -28,6 +28,13 @@ async function installGate(slug, man, facts) {
|
|
|
28
28
|
if (!(await exists(src))) die(L.add.noSuchGate(slug, `${SELF} doctor`));
|
|
29
29
|
|
|
30
30
|
const rec = { slug, ...parseManifest(await readFile(join(src, "gate.yml"), "utf8")) };
|
|
31
|
+
|
|
32
|
+
// Выведенную запись не ставим. Молча пропустить нельзя — человек пришёл за конкретной
|
|
33
|
+
// проверкой и обязан узнать, кто её заменил; ответ «а что теперь» и есть цена вывода.
|
|
34
|
+
// Проверка ДО копирования: иначе в проекте остаётся папка гейта, которого не будет в манифесте.
|
|
35
|
+
const life = entryLifecycle(rec);
|
|
36
|
+
if (life.state === "deprecated") return { rec, cmd: null, copied: [], declared: false, why: null, retired: life.supersededBy || "" };
|
|
37
|
+
|
|
31
38
|
const dst = join(CWD, PROJECT_GATES, slug);
|
|
32
39
|
await mkdir(dst, { recursive: true });
|
|
33
40
|
const copied = await copyDir(src, dst, { force: false });
|
|
@@ -44,7 +51,13 @@ async function installGate(slug, man, facts) {
|
|
|
44
51
|
let cmd = picked
|
|
45
52
|
.replace(/\{gate\}/g, `${PROJECT_GATES}/${slug}`)
|
|
46
53
|
.replace(/\{dir\}/g, ".");
|
|
47
|
-
|
|
54
|
+
// Отказ, а не смерть. `add` ставит одну запись — там смерть уместна и остаётся в вызывающем.
|
|
55
|
+
// `start` ставит пачку, и падение на одной записи оставляло проект с тремя сторожами вместо
|
|
56
|
+
// двенадцати, без единого слова про остальные девять. Найдено прогоном на Windows: там нет
|
|
57
|
+
// ни `ruff`, ни `vulture`, и установка обрывалась на записи `dead-code`, которой нужен
|
|
58
|
+
// настоящий инструмент. Отсутствие сигнала неотличимо от успеха — здесь оно было внутри
|
|
59
|
+
// самой установки.
|
|
60
|
+
if (!cmd) return { rec, cmd: null, copied, declared: false, why: null, noRecipe: true };
|
|
48
61
|
|
|
49
62
|
// Родной инструмент не знает про наши образцы и выдаёт их как находки — в любом проекте,
|
|
50
63
|
// куда поставили гейты. Заворачиваем его в общий фильтр. Переносимая проверка фильтрует
|
|
@@ -81,7 +94,9 @@ async function cmdAdd(args) {
|
|
|
81
94
|
console.log(c.dim(` ${L.add.installAnyway}\n`));
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
const { cmd, copied, declared, why } = await installGate(slug, man, facts);
|
|
97
|
+
const { cmd, copied, declared, why, noRecipe, retired } = await installGate(slug, man, facts);
|
|
98
|
+
if (retired !== undefined) die(L.lifecycle.installDeprecated(slug, retired ? `${SELF} add ${retired}` : "—"));
|
|
99
|
+
if (noRecipe) die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack));
|
|
85
100
|
|
|
86
101
|
console.log(c.bold(`\naqk add ${slug}\n`));
|
|
87
102
|
console.log(` ${c.green("✔")} ${PROJECT_GATES}/${slug}/ ${c.dim(L.add.copied(copied.length))}`);
|
|
@@ -6,8 +6,7 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { join, dirname, relative } from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO_URL, c, exists, die,
|
|
9
|
-
copyDir, writeIfAbsent, FEEDBACK_MARK,
|
|
10
|
-
} from "../lib/core.mjs";
|
|
9
|
+
copyDir, writeIfAbsent, FEEDBACK_MARK, docPath } from "../lib/core.mjs";
|
|
11
10
|
import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
|
|
12
11
|
import { readManifest } from "../lib/manifest.mjs";
|
|
13
12
|
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
@@ -199,7 +198,7 @@ async function cmdBlob() {
|
|
|
199
198
|
await walk(dir);
|
|
200
199
|
|
|
201
200
|
for (const full of found) {
|
|
202
|
-
out += `\n\n${"=".repeat(78)}\n<!-- ${L.blob.source(
|
|
201
|
+
out += `\n\n${"=".repeat(78)}\n<!-- ${L.blob.source(docPath(PKG_ROOT, full))} -->\n${"=".repeat(78)}\n\n`;
|
|
203
202
|
// Ссылки на соседние файлы в склейке ведут в никуда: соседей рядом больше нет,
|
|
204
203
|
// все они внутри этого же текста. Оставляем подпись, снимаем разметку.
|
|
205
204
|
const body = (await readFile(full, "utf8")).replace(
|
|
@@ -268,7 +267,17 @@ async function cmdStart(args) {
|
|
|
268
267
|
if (declared.has(rec.slug)) continue;
|
|
269
268
|
const v = triggerVerdict(rec, facts0);
|
|
270
269
|
if (!v.applies) { skipped.push([rec.slug, v.why]); continue; }
|
|
271
|
-
const { cmd } = await installGate(rec.slug, man, facts0);
|
|
270
|
+
const { cmd, noRecipe, retired } = await installGate(rec.slug, man, facts0);
|
|
271
|
+
// Выведенная запись в пачку не идёт, но и молчать о ней нельзя: она попадает в тот же
|
|
272
|
+
// список пропущенного с названным преемником.
|
|
273
|
+
if (retired !== undefined) {
|
|
274
|
+
skipped.push([rec.slug, L.lifecycle.installDeprecated(rec.slug, retired || "—")]);
|
|
275
|
+
declared.add(rec.slug); continue;
|
|
276
|
+
}
|
|
277
|
+
// Записи, которой нужен инструмент, а его на машине нет, здесь не место — но и вся
|
|
278
|
+
// установка из-за неё останавливаться не должна. Причина называется вслух и попадает
|
|
279
|
+
// в тот же список пропущенного, что и записи, не подошедшие по триггеру.
|
|
280
|
+
if (noRecipe) { skipped.push([rec.slug, L.start.noRecipeHere]); declared.add(rec.slug); continue; }
|
|
272
281
|
put.push([rec.slug, cmd, rec.intent || ""]);
|
|
273
282
|
declared.add(rec.slug);
|
|
274
283
|
added++;
|
package/tool/commands/report.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
|
|
17
17
|
import { join, relative } from "node:path";
|
|
18
|
-
import { CWD, TARGET_DIR, SELF, c, exists } from "../lib/core.mjs";
|
|
18
|
+
import { CWD, TARGET_DIR, SELF, c, exists, docPath } from "../lib/core.mjs";
|
|
19
19
|
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
20
20
|
import { detectFacts, readCatalog, triggerVerdict, whichSync } from "../lib/repo.mjs";
|
|
21
21
|
import { runGates, declaredGates } from "./doctor.mjs";
|
|
@@ -59,7 +59,7 @@ async function findDoc(name) {
|
|
|
59
59
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
60
60
|
const full = join(dir, e.name);
|
|
61
61
|
if (e.isDirectory()) { const hit = await walk(full); if (hit) return hit; }
|
|
62
|
-
else if (e.name === name) return
|
|
62
|
+
else if (e.name === name) return docPath(CWD, full);
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
};
|
package/tool/i18n/en.mjs
CHANGED
|
@@ -15,6 +15,7 @@ export const en = {
|
|
|
15
15
|
start: "no code yet: day-zero guards and the order of work",
|
|
16
16
|
doctor: "check what is laid out and what is missing",
|
|
17
17
|
doctorRun: "and also run the declared gates",
|
|
18
|
+
doctorSince: "the same, but show only what the diff against a ref introduced",
|
|
18
19
|
add: "install a gate from the catalogue into the project",
|
|
19
20
|
find: "is there already such a gate — matched by intent",
|
|
20
21
|
why: "a bug slipped through — why did no guard catch it",
|
|
@@ -66,6 +67,10 @@ export const en = {
|
|
|
66
67
|
totalTodo: (n) => `applicable but not installed ${n}`,
|
|
67
68
|
totalSkip: (n) => `hidden ${n}`,
|
|
68
69
|
|
|
70
|
+
sinceHeading: (ref, n) => `narrowed to the diff against ${ref}: ${n} files touched`,
|
|
71
|
+
sinceBadRef: (ref) => `cannot compare against "${ref}": no such ref, or this is not a git repository`,
|
|
72
|
+
notScopable: "output carries no paths — cannot be narrowed by diff, left red",
|
|
73
|
+
outsideDiff: (n) => `findings exist, but outside the diff (${n})`,
|
|
69
74
|
runHeading: "Running the declared gates",
|
|
70
75
|
timeout: "did not finish within 5 minutes",
|
|
71
76
|
exitCode: (code) => `exit ${code}`,
|
|
@@ -73,8 +78,44 @@ export const en = {
|
|
|
73
78
|
declaredNotRun: (n) => `${n} gates declared, but never run.`,
|
|
74
79
|
declaredNotRunWhy: (cmd) => ` "declared" and "works" are different claims: ${cmd}`,
|
|
75
80
|
|
|
81
|
+
manifestUnknown: (keys) =>
|
|
82
|
+
`The manifest has fields the standard does not know: ${keys.join(", ")}. Looks like a typo — ` +
|
|
83
|
+
`such a field is silently read as absent, and the verdict comes out wrong.`,
|
|
84
|
+
manifestKnown: (keys) => `Manifest fields: ${keys.join(", ")}`,
|
|
76
85
|
thresholdPass: (min) => `Threshold AQK-${min} passed.`,
|
|
77
86
|
thresholdFail: (min, now) => `Threshold AQK-${min} NOT passed: currently AQK-${now}.`,
|
|
87
|
+
thresholdGateFail: (min, now, names) =>
|
|
88
|
+
`Threshold AQK-${min} passed (currently AQK-${now}), but a gate failed: ${names.join(", ")}.`,
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
baseline: {
|
|
92
|
+
heading: "The minimum a project needs",
|
|
93
|
+
intro: (checked, total) =>
|
|
94
|
+
`a machine confirms ${checked} of ${total} points; the rest are for your eyes, in the guide`,
|
|
95
|
+
eyes: (n, path) => `${n} points a machine cannot check — they live in ${path}`,
|
|
96
|
+
caveat: "presence is what gets checked, not whether it works: \"a linter is configured\" and \"a linter catches things\" are different claims",
|
|
97
|
+
by: (b) =>
|
|
98
|
+
"proven by: " +
|
|
99
|
+
({ gate: `gate ${b.value}`, fact: `repository scan: ${b.value}`,
|
|
100
|
+
manifest: `field ${b.value} in the manifest`, dep: `dependency ${b.value}`,
|
|
101
|
+
file: b.value }[b.kind] || b.value),
|
|
102
|
+
none: "no conventional marker — check by eye, it may be done another way",
|
|
103
|
+
titles: {
|
|
104
|
+
oneCommand: "one command brings the whole project up",
|
|
105
|
+
lockfile: "exact versions pinned in a lockfile",
|
|
106
|
+
sameEnv: "the environment is the same for everyone and in CI",
|
|
107
|
+
formatter: "formatting is uniform and applied automatically",
|
|
108
|
+
linter: "a linter is configured",
|
|
109
|
+
types: "type checking exists",
|
|
110
|
+
secretScan: "secret scanning",
|
|
111
|
+
fileSize: "a file size limit",
|
|
112
|
+
ownInvariants: "the project's own invariants",
|
|
113
|
+
tests: "arbiters of correctness: tests exist",
|
|
114
|
+
pipeline: "a pipeline exists",
|
|
115
|
+
errorTracker: "errors are collected separately from logs",
|
|
116
|
+
machineReadable: "the project is machine-readable",
|
|
117
|
+
rulesInRepo: "rules live in the repository and are versioned",
|
|
118
|
+
},
|
|
78
119
|
},
|
|
79
120
|
|
|
80
121
|
trigger: {
|
|
@@ -91,6 +132,7 @@ export const en = {
|
|
|
91
132
|
has_deps: ["no dependency file in sight", "dependencies are declared"],
|
|
92
133
|
has_tests: ["no tests in sight", "tests exist"],
|
|
93
134
|
has_env: ["no environment file", "an environment file exists"],
|
|
135
|
+
has_ui: ["no stylesheets or UI components in sight", "a UI exists: stylesheets or components"],
|
|
94
136
|
},
|
|
95
137
|
},
|
|
96
138
|
|
|
@@ -99,6 +141,18 @@ export const en = {
|
|
|
99
141
|
none: "no recipe described",
|
|
100
142
|
},
|
|
101
143
|
|
|
144
|
+
// Entry maturity. Computed from the entry's proof; it cannot be declared — see
|
|
145
|
+
// entryLifecycle in tool/lib/manifest.mjs.
|
|
146
|
+
lifecycle: {
|
|
147
|
+
stable: "proven by an incident from the journal",
|
|
148
|
+
experimental: "proof is not from the journal — the entry is provisional",
|
|
149
|
+
deprecated: "retired",
|
|
150
|
+
unknownReplacement: (v) => `superseded_by: ${v} — no such entry in the catalogue`,
|
|
151
|
+
noReplacement: "lifecycle: deprecated without superseded_by — no replacement is named",
|
|
152
|
+
notDeclarable: (v) => `lifecycle: ${v} cannot be declared — maturity is computed from the proof`,
|
|
153
|
+
unknown: (v) => `lifecycle: ${v} — no such state; only deprecated is declared`,
|
|
154
|
+
installDeprecated: (slug, by) => `entry ${slug} is retired, ${by} replaces it`,
|
|
155
|
+
},
|
|
102
156
|
manifest: {
|
|
103
157
|
noGatesBlock: "no gates: block in .aqk.yml",
|
|
104
158
|
alreadyDeclared: "already declared",
|
|
@@ -299,6 +353,7 @@ export const en = {
|
|
|
299
353
|
},
|
|
300
354
|
|
|
301
355
|
start: {
|
|
356
|
+
noRecipeHere: "needs a tool that is not on this machine",
|
|
302
357
|
initFailed: (cmd) => `Could not lay out the kit. Start with ${cmd}`,
|
|
303
358
|
tooManyFiles: (n) => `This repository already has ${n} code files — that is a different scenario.`,
|
|
304
359
|
useDoctor: (cmd) => `${cmd} will inspect what is here and split the entries into three lists:`,
|
package/tool/i18n/ru.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export const ru = {
|
|
|
16
16
|
start: "кода ещё нет: сторожа дня 0 и порядок работы",
|
|
17
17
|
doctor: "проверить, что разложено и чего не хватает",
|
|
18
18
|
doctorRun: "ещё и запустить объявленные гейты",
|
|
19
|
+
doctorSince: "то же, но показать только то, что внёс диф относительно ссылки",
|
|
19
20
|
add: "поставить гейт из каталога в проект",
|
|
20
21
|
find: "есть ли уже такой гейт — сверка по намерению",
|
|
21
22
|
why: "поймал ошибку — почему её не поймал сторож",
|
|
@@ -67,6 +68,10 @@ export const ru = {
|
|
|
67
68
|
totalTodo: (n) => `применимо но не поставлено ${n}`,
|
|
68
69
|
totalSkip: (n) => `скрыто ${n}`,
|
|
69
70
|
|
|
71
|
+
sinceHeading: (ref, n) => `сужено до дифа относительно ${ref}: файлов затронуто ${n}`,
|
|
72
|
+
sinceBadRef: (ref) => `не могу сравнить с «${ref}»: такой ссылки нет или это не репозиторий git`,
|
|
73
|
+
notScopable: "вывод без путей — дифом не сужается, оставлен красным",
|
|
74
|
+
outsideDiff: (n) => `находки есть, но вне дифа (${n})`,
|
|
70
75
|
runHeading: "Прогон объявленных гейтов",
|
|
71
76
|
timeout: "не уложился в 5 минут",
|
|
72
77
|
exitCode: (code) => `код ${code}`,
|
|
@@ -74,8 +79,49 @@ export const ru = {
|
|
|
74
79
|
declaredNotRun: (n) => `${n} гейтов объявлено, но не запускалось.`,
|
|
75
80
|
declaredNotRunWhy: (cmd) => ` «Объявлен» и «работает» — разные утверждения: ${cmd}`,
|
|
76
81
|
|
|
82
|
+
manifestUnknown: (keys) =>
|
|
83
|
+
`В манифесте поля, которых стандарт не знает: ${keys.join(", ")}. Похоже на опечатку — ` +
|
|
84
|
+
`такое поле молча читается как отсутствующее, и вердикт выходит неверным.`,
|
|
85
|
+
manifestKnown: (keys) => `Поля манифеста: ${keys.join(", ")}`,
|
|
77
86
|
thresholdPass: (min) => `Порог AQK-${min} пройден.`,
|
|
78
87
|
thresholdFail: (min, now) => `Порог AQK-${min} НЕ пройден: сейчас AQK-${now}.`,
|
|
88
|
+
// Ступень взята, но прогон красный — это другое утверждение, и виновника называем сразу:
|
|
89
|
+
// иначе его ищут глазами выше по логу конвейера.
|
|
90
|
+
thresholdGateFail: (min, now, names) =>
|
|
91
|
+
`Порог AQK-${min} пройден (сейчас AQK-${now}), но упал гейт: ${names.join(", ")}.`,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
baseline: {
|
|
95
|
+
heading: "Обязательный минимум проекта",
|
|
96
|
+
intro: (checked, total) =>
|
|
97
|
+
`машина подтверждает ${checked} пунктов из ${total}; остальные — глазами, по методичке`,
|
|
98
|
+
eyes: (n, path) => `${n} пунктов машина проверить не может — они в ${path}`,
|
|
99
|
+
caveat: "проверяется НАЛИЧИЕ признака, а не то, что он работает: «линтер настроен» и «линтер ловит» — разные утверждения",
|
|
100
|
+
by: (b) =>
|
|
101
|
+
"подтверждено: " +
|
|
102
|
+
({ gate: `гейт ${b.value}`, fact: `осмотр репозитория: ${b.value}`,
|
|
103
|
+
manifest: `поле ${b.value} в манифесте`, dep: `зависимость ${b.value}`,
|
|
104
|
+
file: b.value }[b.kind] || b.value),
|
|
105
|
+
// «Не найдено» и «нет» — разные утверждения. Признак ищется по общепринятым именам; проект
|
|
106
|
+
// может делать то же самое своим способом, и тогда пункт остаётся человеку, а не считается
|
|
107
|
+
// проваленным. Иначе прибор врёт на первом же нестандартном проекте — на нас самих и врал.
|
|
108
|
+
none: "общепринятого признака нет — проверь глазами, возможно сделано иначе",
|
|
109
|
+
titles: {
|
|
110
|
+
oneCommand: "одна команда поднимает проект целиком",
|
|
111
|
+
lockfile: "точные версии записаны в файл-замок",
|
|
112
|
+
sameEnv: "среда одинакова у всех и в конвейере",
|
|
113
|
+
formatter: "форматирование единое и применяется автоматически",
|
|
114
|
+
linter: "линтер настроен",
|
|
115
|
+
types: "проверка типов есть",
|
|
116
|
+
secretScan: "поиск секретов",
|
|
117
|
+
fileSize: "ограничение размера файла",
|
|
118
|
+
ownInvariants: "свои инварианты проекта",
|
|
119
|
+
tests: "арбитры правильности: тесты есть",
|
|
120
|
+
pipeline: "конвейер есть",
|
|
121
|
+
errorTracker: "ошибки собираются отдельно от логов",
|
|
122
|
+
machineReadable: "проект читается машиной",
|
|
123
|
+
rulesInRepo: "правила живут в репозитории и версионируются",
|
|
124
|
+
},
|
|
79
125
|
},
|
|
80
126
|
|
|
81
127
|
trigger: {
|
|
@@ -92,6 +138,7 @@ export const ru = {
|
|
|
92
138
|
has_deps: ["не видно файла зависимостей", "зависимости объявлены"],
|
|
93
139
|
has_tests: ["не видно тестов", "тесты есть"],
|
|
94
140
|
has_env: ["нет файла окружения", "файл окружения есть"],
|
|
141
|
+
has_ui: ["не видно стилей и компонентов интерфейса", "интерфейс есть: стили или компоненты"],
|
|
95
142
|
},
|
|
96
143
|
},
|
|
97
144
|
|
|
@@ -100,6 +147,19 @@ export const ru = {
|
|
|
100
147
|
none: "рецепт не описан",
|
|
101
148
|
},
|
|
102
149
|
|
|
150
|
+
// Зрелость записи каталога. Считается по доказательству; объявить её нельзя — см.
|
|
151
|
+
// entryLifecycle в tool/lib/manifest.mjs.
|
|
152
|
+
lifecycle: {
|
|
153
|
+
stable: "доказана шишкой из журнала",
|
|
154
|
+
experimental: "доказательство не из журнала — запись условная",
|
|
155
|
+
deprecated: "выведена из употребления",
|
|
156
|
+
unknownReplacement: (v) => `superseded_by: ${v} — такой записи в каталоге нет`,
|
|
157
|
+
noReplacement: "lifecycle: deprecated без superseded_by — не назван тот, кто заменяет",
|
|
158
|
+
notDeclarable: (v) => `lifecycle: ${v} объявлять нельзя — зрелость считается по доказательству`,
|
|
159
|
+
unknown: (v) => `lifecycle: ${v} — такого состояния нет; объявляется только deprecated`,
|
|
160
|
+
installDeprecated: (slug, by) =>
|
|
161
|
+
`запись ${slug} выведена из употребления, её заменяет ${by}`,
|
|
162
|
+
},
|
|
103
163
|
manifest: {
|
|
104
164
|
noGatesBlock: "в .aqk.yml нет блока gates:",
|
|
105
165
|
alreadyDeclared: "уже объявлен",
|
|
@@ -300,6 +360,7 @@ export const ru = {
|
|
|
300
360
|
},
|
|
301
361
|
|
|
302
362
|
start: {
|
|
363
|
+
noRecipeHere: "нужен инструмент, которого нет на этой машине",
|
|
303
364
|
initFailed: (cmd) => `Не получилось разложить комплект. Начни с ${cmd}`,
|
|
304
365
|
tooManyFiles: (n) => `В репозитории уже ${n} файлов кода — это другой сценарий.`,
|
|
305
366
|
useDoctor: (cmd) => `${cmd} осмотрит, что есть, и разделит записи на три списка:`,
|
|
@@ -11,17 +11,17 @@ const AGENTS_MD = `# AGENTS.md
|
|
|
11
11
|
|
|
12
12
|
## Hard rules
|
|
13
13
|
|
|
14
|
-
- **A plan before code.** A non-trivial task starts with a plan a human approved in words.
|
|
15
|
-
- **A red test before code.** First a check that fails, then the implementation.
|
|
16
|
-
- **Three attempts maximum.** Not solved in three — stop and ask a human, not a fourth try.
|
|
17
|
-
- **Secrets only in the environment.** Never in code, logs or commits.
|
|
18
|
-
- **Only the files the task is about.** No fixing things "while we are here".
|
|
19
|
-
- **Done = proven.** Name the arbiter: a test, a live run, a check against the source.
|
|
14
|
+
- **A plan before code.** A non-trivial task starts with a plan a human approved in words. <!-- aqk: human -->
|
|
15
|
+
- **A red test before code.** First a check that fails, then the implementation. <!-- aqk: human -->
|
|
16
|
+
- **Three attempts maximum.** Not solved in three — stop and ask a human, not a fourth try. <!-- aqk: human -->
|
|
17
|
+
- **Secrets only in the environment.** Never in code, logs or commits. <!-- aqk: secrets-not-in-code -->
|
|
18
|
+
- **Only the files the task is about.** No fixing things "while we are here". <!-- aqk: human -->
|
|
19
|
+
- **Done = proven.** Name the arbiter: a test, a live run, a check against the source. <!-- aqk: human -->
|
|
20
20
|
"Looks like it works" is not done.
|
|
21
|
-
- **Never swallow an error.** Either handled and logged, or re-raised.
|
|
22
|
-
- **A fork in the road is a question for a human.** Departing from an agreed decision is not
|
|
21
|
+
- **Never swallow an error.** Either handled and logged, or re-raised. <!-- aqk: swallowed-error -->
|
|
22
|
+
- **A fork in the road is a question for a human.** Departing from an agreed decision is not <!-- aqk: human -->
|
|
23
23
|
documented with a code comment.
|
|
24
|
-
- **Report on your work with the kit with a command, not with words.** When you are done, run
|
|
24
|
+
- **Report on your work with the kit with a command, not with words.** When you are done, run <!-- aqk: human -->
|
|
25
25
|
\`aqk report\`. It is assembled from an actual run: a summary from memory always picks the
|
|
26
26
|
convenient parts and stays quiet about a gate standing on the weakest recipe.
|
|
27
27
|
|