@rt-tools/agent-kit 0.8.2 → 0.9.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 +13 -0
- package/assets/agents/rules-reviewer.md +83 -0
- package/assets/checks/board.github.mjs +48 -1
- package/assets/checks/check-board.github.mjs +84 -1
- package/assets/checks/check-dupes.mjs +66 -6
- package/assets/checks/check-lib-layers.mjs +13 -524
- package/assets/checks/check-specs.mjs +61 -747
- package/assets/checks/check-styles.mjs +185 -15
- package/assets/checks/lib-boundaries.mjs +143 -0
- package/assets/checks/lib-common.mjs +149 -0
- package/assets/checks/lib-domains.mjs +205 -0
- package/assets/checks/lib-manifests.mjs +60 -0
- package/assets/checks/lib-reexports.mjs +101 -0
- package/assets/checks/rt-kit-checks.config.mjs +26 -0
- package/assets/checks/spec-anchors.mjs +297 -0
- package/assets/checks/spec-common.mjs +222 -0
- package/assets/checks/spec-contract.mjs +152 -0
- package/assets/checks/spec-scenarios.mjs +201 -0
- package/assets/commands/feedback.md +95 -0
- package/assets/commands/rules-review.md +98 -0
- package/assets/commands/skill-curator.md +39 -22
- package/assets/defaults/project.sh +8 -0
- package/assets/docs/GLOSSARY.md +21 -20
- package/assets/hooks/git-guard-push-tests.sh +8 -4
- package/assets/hooks/reuse-first-guard.sh +16 -2
- package/assets/hooks/skill-gate.sh +1 -1
- package/assets/hooks/sql-guard-parse.sh +187 -0
- package/assets/hooks/sql-guard-request.sh +117 -0
- package/assets/hooks/sql-guard-target.sh +134 -0
- package/assets/hooks/sql-guard-write.sh +212 -0
- package/assets/hooks/sql-guard.sh +26 -596
- package/assets/hooks/task-flow-guard.sh +16 -2
- package/assets/hooks/waiting-turn-guard.sh +116 -0
- package/assets/laws/delivery.md +35 -0
- package/assets/laws/project-documentation.md +18 -0
- package/assets/laws/work-conduct.md +25 -0
- package/assets/patterns/admin-lists-screen.md +25 -14
- package/assets/patterns/admin-nav-item.md +1 -1
- package/assets/patterns/component-structure-new.md +1 -1
- package/assets/patterns/entity-aside.md +4 -2
- package/assets/patterns/git-workflow-commit.azure.md +74 -2
- package/assets/patterns/git-workflow-commit.github.md +75 -2
- package/assets/patterns/git-workflow-commit.gitlab.md +75 -4
- package/assets/patterns/git-workflow-docker.md +30 -0
- package/assets/patterns/observability-record.md +9 -0
- package/assets/patterns/shared-code-new.md +2 -2
- package/assets/patterns/task-flow-close.md +160 -47
- package/assets/patterns/task-flow-handoff.md +1 -1
- package/assets/patterns/task-flow-resume.md +3 -3
- package/assets/patterns/task-flow-start.md +32 -5
- package/assets/rules/angular-patterns.md +22 -0
- package/assets/rules/api-layer.md +25 -0
- package/assets/rules/browser-verification.md +32 -0
- package/assets/rules/component-structure.md +21 -0
- package/assets/rules/dependencies.md +22 -0
- package/assets/rules/doc-style.md +24 -0
- package/assets/rules/entity-conventions.needs-admin.md +21 -0
- package/assets/rules/entity-models.md +21 -0
- package/assets/rules/git-workflow.azure.md +57 -1
- package/assets/rules/git-workflow.github.md +89 -2
- package/assets/rules/git-workflow.gitlab.md +56 -1
- package/assets/rules/lib-layers.md +29 -0
- package/assets/rules/lists.md +37 -10
- package/assets/rules/navigation.md +21 -0
- package/assets/rules/observability.needs-app.md +23 -0
- package/assets/rules/permissions.md +23 -0
- package/assets/rules/platform-access.md +21 -0
- package/assets/rules/reuse-first.md +20 -0
- package/assets/rules/seo.md +19 -0
- package/assets/rules/shared-code.md +20 -1
- package/assets/rules/spec-driven.md +32 -0
- package/assets/rules/styling-bem.md +19 -0
- package/assets/rules/task-flow.md +148 -18
- package/assets/rules/testing.md +62 -0
- package/assets/rules/translations.md +21 -0
- package/assets/rules/typescript-conventions.md +28 -0
- package/assets/samples/specs/_template/spec.md +83 -0
- package/assets/samples/tasks/_template/grill.md +28 -0
- package/assets/samples/tasks/_template/plan.md +39 -0
- package/assets/samples/tasks/_template/progress.md +23 -0
- package/assets/skills/agent-kit.md +52 -2
- package/assets/templates/proposal.md +21 -0
- package/assets/templates/rule.md +31 -2
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +115 -87
- package/bin/agent-kit.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/argv.d.ts.map +1 -1
- package/lib/argv.js +6 -4
- package/lib/argv.js.map +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +2 -1
- package/lib/assets.js.map +1 -1
- package/lib/cargo.d.ts +20 -0
- package/lib/cargo.d.ts.map +1 -1
- package/lib/cargo.js.map +1 -1
- package/lib/cascade.d.ts +55 -0
- package/lib/cascade.d.ts.map +1 -0
- package/lib/cascade.js +131 -0
- package/lib/cascade.js.map +1 -0
- package/lib/catalog.d.ts +0 -75
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +44 -127
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +153 -85
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +5 -5
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +12 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +11 -5
- package/lib/config.js.map +1 -1
- package/lib/enroll.d.ts +56 -0
- package/lib/enroll.d.ts.map +1 -0
- package/lib/enroll.js +123 -0
- package/lib/enroll.js.map +1 -0
- package/lib/freshness.d.ts.map +1 -1
- package/lib/freshness.js +31 -17
- package/lib/freshness.js.map +1 -1
- package/lib/hooks-map.d.ts +30 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +80 -18
- package/lib/hooks-map.js.map +1 -1
- package/lib/integrity.d.ts +1 -2
- package/lib/integrity.d.ts.map +1 -1
- package/lib/integrity.js +0 -1
- package/lib/integrity.js.map +1 -1
- package/lib/observations.d.ts.map +1 -1
- package/lib/observations.js +25 -12
- package/lib/observations.js.map +1 -1
- package/lib/order.d.ts +10 -0
- package/lib/order.d.ts.map +1 -0
- package/lib/order.js +14 -0
- package/lib/order.js.map +1 -0
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +8 -2
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +1 -1
- package/lib/plan.js.map +1 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +25 -8
- package/lib/proposals.js.map +1 -1
- package/lib/sections.js +1 -1
- package/lib/sections.js.map +1 -1
- package/lib/ship.d.ts.map +1 -1
- package/lib/ship.js +9 -1
- package/lib/ship.js.map +1 -1
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.js +14 -10
- package/lib/shipment.js.map +1 -1
- package/lib/snapshot.d.ts.map +1 -1
- package/lib/snapshot.js +2 -1
- package/lib/snapshot.js.map +1 -1
- package/lib/stamp.js +1 -1
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +12 -2
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +11 -10
- package/lib/sync.js.map +1 -1
- package/lib/vars.d.ts.map +1 -1
- package/lib/vars.js +2 -3
- package/lib/vars.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.9.0.tgz +0 -0
- package/rt-tools-agent-kit-0.8.2.tgz +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Привязка правила к коду и законы, которые применяет спек.
|
|
3
|
+
*
|
|
4
|
+
* Правило живёт в спеке, привязка — в компаньоне рядом; ключ связи — сам текст правила.
|
|
5
|
+
* Здесь же разбирается мёртвая привязка: символ, объявленный в своём файле и больше нигде не
|
|
6
|
+
* встречающийся, местом исполнения правила не считается.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
ANCHOR,
|
|
10
|
+
BACKTICKED,
|
|
11
|
+
CONSTITUTION_DIR,
|
|
12
|
+
SOURCE_ROOTS,
|
|
13
|
+
VERDICT,
|
|
14
|
+
VERDICT_MIN,
|
|
15
|
+
bulletsOf,
|
|
16
|
+
exists,
|
|
17
|
+
read,
|
|
18
|
+
report,
|
|
19
|
+
sectionOf,
|
|
20
|
+
walk,
|
|
21
|
+
} from './spec-common.mjs';
|
|
22
|
+
|
|
23
|
+
// ── 1. Якоря правил ────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Символ ищется как слово: подстрока дала бы ложное совпадение на префиксе.
|
|
29
|
+
*
|
|
30
|
+
* Границы слова считаются буквой любого алфавита, а не `\b`: он в JavaScript знает буквой
|
|
31
|
+
* только латиницу, и `\bСемья\b` не совпадает ни разу — привязка на русском слове читалась
|
|
32
|
+
* как ведущая в файл, где этого слова нет, при том что слово стоит там первой же строкой.
|
|
33
|
+
*/
|
|
34
|
+
function fileHasSymbol(path, symbol) {
|
|
35
|
+
// Дефис здесь не экранируется: вне класса символов он ничего не значит, а под флагом `u`
|
|
36
|
+
// лишнее экранирование — уже отказ разбора. Общий экранировщик его защищает, потому что
|
|
37
|
+
// рассчитан и на класс тоже, и `task-flow` роняло всю сверку целиком.
|
|
38
|
+
const word = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])${word}(?![\\p{L}\\p{N}_])`, 'u').test(read(path));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Привязки, у которых остаётся выяснить, зовёт ли символ хоть кто-нибудь. Копятся
|
|
44
|
+
* в один список и разбираются одним проходом по исходникам: обходить `apps` и
|
|
45
|
+
* `libs` на каждую из пятисот привязок было бы полтысячи обходов.
|
|
46
|
+
*/
|
|
47
|
+
const traced = [];
|
|
48
|
+
|
|
49
|
+
/** Жирное начало пункта — ключ, по которому правило находит свою строку привязки. */
|
|
50
|
+
function ruleHeadOf(bulletText) {
|
|
51
|
+
const bold = bulletText.match(/\*\*(.+?)\*\*/s);
|
|
52
|
+
|
|
53
|
+
return bold ? bold[1].replace(/\s+/g, ' ').trim() : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Правила живут в `spec.md`, привязка к коду — в `implementation.md` рядом. Разделены
|
|
58
|
+
* потому, что спек описывает продукт и читается без знания устройства, а привязка
|
|
59
|
+
* устаревает при каждом переименовании.
|
|
60
|
+
*
|
|
61
|
+
* Ключ связи — сам текст правила, а не отдельный идентификатор: тогда правку формулировки
|
|
62
|
+
* невозможно сделать, забыв про привязку, — строка перестанет находиться.
|
|
63
|
+
*
|
|
64
|
+
* Заголовок раздела приходит доводом: у спека это `## Правила`, у закона — `## Статьи`.
|
|
65
|
+
* Одно слово в двух смыслах развели именно здесь: «правило» — слой между законом и скилом,
|
|
66
|
+
* а внутри закона живут статьи.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Строки таблицы привязок компаньона.
|
|
70
|
+
*
|
|
71
|
+
* Компаньон правила держит три таблицы: чем вещи правила названы в этом дереве, где лежат
|
|
72
|
+
* механизмы и где исполняется каждая статья. Привязки — только третья, и берётся она по имени
|
|
73
|
+
* раздела, а не по месту в файле. Пока читался весь файл, строки первых двух попадали в список
|
|
74
|
+
* наравне с настоящими и тут же объявлялись расхождением: статьи с таким текстом в правиле нет
|
|
75
|
+
* и быть не может. Две трети перечня в дереве были ими, и правильно дописанная строка «Где это
|
|
76
|
+
* лежит» отвечала отказом.
|
|
77
|
+
*
|
|
78
|
+
* У компаньона спека домена раздела нет: там таблица одна, и сужать нечего — такой зовёт без
|
|
79
|
+
* имени раздела. У правила раздел стоит в образце компаньона, поэтому его отсутствие — отказ:
|
|
80
|
+
* молча прочесть вместо него весь файл значило бы вернуть тот же дефект.
|
|
81
|
+
*/
|
|
82
|
+
function rowsOfMap(specFile, mapFile, mapHeading) {
|
|
83
|
+
const text = read(mapFile);
|
|
84
|
+
if (!mapHeading) {
|
|
85
|
+
return text.split('\n');
|
|
86
|
+
}
|
|
87
|
+
const section = sectionOf(text, mapHeading);
|
|
88
|
+
if (!section.length) {
|
|
89
|
+
report(mapFile, `нет раздела \`${mapHeading}\` — привязкам правила негде лежать`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return section;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function checkRuleImplementation(specFile, text, mapFile, heading = '## Правила', mapHeading = '') {
|
|
96
|
+
const bullets = bulletsOf(sectionOf(text, heading));
|
|
97
|
+
if (!bullets.length) {
|
|
98
|
+
report(specFile, `в разделе \`${heading}\` нет ни одного пункта`);
|
|
99
|
+
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!exists(mapFile)) {
|
|
104
|
+
report(specFile, `нет файла \`${mapFile.split('/').pop()}\` рядом — правилам не к чему привязаться`);
|
|
105
|
+
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rows = new Map();
|
|
110
|
+
for (const line of rowsOfMap(specFile, mapFile, mapHeading)) {
|
|
111
|
+
const cells = line.match(/^\|([^|]+)\|([^|]*)\|\s*$/);
|
|
112
|
+
if (!cells) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const head = cells[1].replace(/\s+/g, ' ').trim();
|
|
116
|
+
// Шапка таблицы: у спека колонка называется «Правило», у закона — «Статья».
|
|
117
|
+
if (!head || head === 'Правило' || head === 'Статья' || /^-+$/.test(head)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const cell = cells[2].trim();
|
|
121
|
+
rows.set(head, {
|
|
122
|
+
anchors: [...cells[2].matchAll(ANCHOR)],
|
|
123
|
+
verdict: VERDICT.test(cell) && cell.length >= VERDICT_MIN,
|
|
124
|
+
used: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const bullet of bullets) {
|
|
129
|
+
const head = ruleHeadOf(bullet.text);
|
|
130
|
+
if (!head) {
|
|
131
|
+
report(specFile, `правило без жирного начала: «${bullet.text.replace(/^-\s+/, '').slice(0, 60)}…»`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const row = rows.get(head);
|
|
135
|
+
if (!row) {
|
|
136
|
+
report(
|
|
137
|
+
mapFile,
|
|
138
|
+
`правило без привязки: «${head.slice(0, 60)}…» — допиши строку с \`файл:символ\`, ` +
|
|
139
|
+
'вердиктом «Не исполняется» с причиной либо перенеси правило в «Открытые вопросы» как Q-N'
|
|
140
|
+
);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
row.used = true;
|
|
144
|
+
if (!row.anchors.length && !row.verdict) {
|
|
145
|
+
report(
|
|
146
|
+
mapFile,
|
|
147
|
+
`у правила «${head.slice(0, 60)}…» пустая привязка — поставь \`файл:символ\` ` +
|
|
148
|
+
'либо вердикт «Не исполняется», «Не применимо», «Не проверяется» с причиной'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
for (const [, path, symbol] of row.anchors) {
|
|
152
|
+
if (!exists(path)) {
|
|
153
|
+
report(mapFile, `привязка ведёт в никуда: нет файла \`${path}\``);
|
|
154
|
+
} else if (!fileHasSymbol(path, symbol)) {
|
|
155
|
+
report(mapFile, `привязка не сходится: в \`${path}\` нет \`${symbol}\``);
|
|
156
|
+
} else {
|
|
157
|
+
traced.push({ mapFile, path, symbol });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Обратная сторона: строка, под которой правила больше нет, — след переименования.
|
|
163
|
+
// Без неё привязка копится и начинает описывать несуществующие обещания
|
|
164
|
+
for (const [head, row] of rows) {
|
|
165
|
+
if (!row.used) {
|
|
166
|
+
report(mapFile, `привязка без пункта: «${head.slice(0, 60)}…» — в \`${specFile}\` такого пункта нет`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── 5. Мёртвые привязки ───────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Код без комментариев. Упоминание символа в пояснении вызовом не является, а
|
|
175
|
+
* пояснений у мёртвого кода как раз обычно больше, чем у живого.
|
|
176
|
+
*/
|
|
177
|
+
const codeOf = (text) => text.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:`'"])\/\/.*$/gm, '$1');
|
|
178
|
+
|
|
179
|
+
const DECLARATION_MODIFIERS = '(?:export|declare|abstract|public|private|protected|static|readonly|override|async|accessor)';
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Объявлен ли символ здесь. Проверка идёт только по объявлениям: привязка к
|
|
183
|
+
* чужому полю (`HttpStatus.SERVICE_UNAVAILABLE`), ключу словаря или содержимому
|
|
184
|
+
* строки законна и встречается ровно один раз по своей природе.
|
|
185
|
+
*/
|
|
186
|
+
function fileDeclaresSymbol(code, symbol) {
|
|
187
|
+
const escaped = escapeForRegExp(symbol);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
new RegExp(`\\b(?:const|let|var|function|class|interface|type|enum)\\s+${escaped}\\b`).test(code) ||
|
|
191
|
+
new RegExp(`^\\s*(?:${DECLARATION_MODIFIERS}\\s+)*#?${escaped}\\s*[(<:=]`, 'm').test(code)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Файлы, в которых встречается каждый символ. Дефис берётся в токен целиком ради
|
|
197
|
+
* атрибутов разметки, а части такого токена добавляются отдельно: иначе
|
|
198
|
+
* `resolving` внутри `data-resolving` перестал бы находиться.
|
|
199
|
+
*/
|
|
200
|
+
function symbolOwners() {
|
|
201
|
+
const owners = new Map();
|
|
202
|
+
const remember = (token, file) => {
|
|
203
|
+
let files = owners.get(token);
|
|
204
|
+
if (!files) {
|
|
205
|
+
files = new Set();
|
|
206
|
+
owners.set(token, files);
|
|
207
|
+
}
|
|
208
|
+
files.add(file);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
for (const file of SOURCE_ROOTS.flatMap((root) => walk(root, (name) => name.endsWith('.ts') || name.endsWith('.html')))) {
|
|
212
|
+
const text = file.endsWith('.ts') ? codeOf(read(file)) : read(file);
|
|
213
|
+
for (const [token] of text.matchAll(/[A-Za-z_][\w-]*/g)) {
|
|
214
|
+
remember(token, file);
|
|
215
|
+
if (token.includes('-')) {
|
|
216
|
+
token.split('-').forEach((part) => part && remember(part, file));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return owners;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Символ, объявленный в своём файле и больше нигде не встречающийся, ничего не
|
|
226
|
+
* исполняет: правило, привязанное к нему, описывает намерение.
|
|
227
|
+
*/
|
|
228
|
+
function checkTracedAnchors() {
|
|
229
|
+
const code = new Map();
|
|
230
|
+
const codeAt = (path) => {
|
|
231
|
+
if (!code.has(path)) {
|
|
232
|
+
code.set(path, codeOf(read(path)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return code.get(path);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const declared = traced.filter(({ path, symbol }) => path.endsWith('.ts') && fileDeclaresSymbol(codeAt(path), symbol));
|
|
239
|
+
if (!declared.length) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const owners = symbolOwners();
|
|
244
|
+
for (const { mapFile, path, symbol } of declared) {
|
|
245
|
+
const here = (codeAt(path).match(new RegExp(`\\b${escapeForRegExp(symbol)}\\b`, 'g')) || []).length;
|
|
246
|
+
const elsewhere = [...(owners.get(symbol) || [])].filter((file) => file !== path).length;
|
|
247
|
+
if (here + elsewhere < 2) {
|
|
248
|
+
report(
|
|
249
|
+
mapFile,
|
|
250
|
+
`привязка ведёт в мёртвый код: \`${symbol}\` объявлен в \`${path}\` и больше нигде не встречается — ` +
|
|
251
|
+
'либо правило исполняется в другом месте, либо ему место в «Открытых вопросах» как Q-N'
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── 1a. Законы, которые применяет спек ────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Связь «закон — правило» и «правило — паттерн» сверяется в обе стороны, а спек до сих пор
|
|
261
|
+
* говорил только о домене. Закон при этом он применял: ссылки на `docs/constitution/…`
|
|
262
|
+
* лежали внутри строки зависимостей и посреди текста, и по закону нельзя было узнать, какие
|
|
263
|
+
* домены на нём стоят, — только грепом.
|
|
264
|
+
*
|
|
265
|
+
* Отсюда строка `**Законы:**` в шапке и сверка обеих сторон: закон, названный в тексте, но не
|
|
266
|
+
* объявленный, и объявленный закон, которого нет.
|
|
267
|
+
*/
|
|
268
|
+
const SPEC_LAWS = /^\*\*Законы:\*\*\s*(.+)$/;
|
|
269
|
+
/**
|
|
270
|
+
* Ссылка на закон где угодно в тексте спека — по ней считается вторая сторона связи. Слой в
|
|
271
|
+
* пути необязателен: законы приложения лежат в `application/`, а называются так же.
|
|
272
|
+
*/
|
|
273
|
+
const LAW_REFERENCE = new RegExp(`\`${CONSTITUTION_DIR}/(?:application/)?([a-z-]+)\\.md\``, 'g');
|
|
274
|
+
|
|
275
|
+
function checkSpecLaws(file, text, laws) {
|
|
276
|
+
const line = text.split('\n').find((candidate) => SPEC_LAWS.test(candidate));
|
|
277
|
+
if (!line) {
|
|
278
|
+
report(file, 'в шапке нет строки `**Законы:**` — не видно, какие законы домен применяет');
|
|
279
|
+
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const declared = new Set([...line.match(SPEC_LAWS)[1].matchAll(BACKTICKED)].map(([, name]) => name));
|
|
284
|
+
for (const name of declared) {
|
|
285
|
+
if (!laws.has(name)) {
|
|
286
|
+
report(file, `в строке \`**Законы:**\` назван \`${name}\`, а закона с таким именем нет ни в одном слое`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const [, name] of text.matchAll(LAW_REFERENCE)) {
|
|
291
|
+
if (!declared.has(name)) {
|
|
292
|
+
report(file, `закон \`${name}\` назван в тексте, но не объявлен в строке \`**Законы:**\``);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { checkRuleImplementation, checkSpecLaws, checkTracedAnchors };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Общее для всех предметов сверки спеков: что считается доменом, чем читается дерево и чем
|
|
3
|
+
* режется документ на разделы и пункты.
|
|
4
|
+
*
|
|
5
|
+
* Модуль назван не `check-*`, и это не украшение: умолчание пакета собирает набор гейта пуша
|
|
6
|
+
* перебором имён `check-<что>.mjs` в каталоге проверок, и помощник с таким именем гейт стал бы
|
|
7
|
+
* гонять как отдельную проверку.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
13
|
+
|
|
14
|
+
const SPECS_DIR = CONFIG.specsDir;
|
|
15
|
+
const CONSTITUTION_DIR = 'docs/constitution';
|
|
16
|
+
/**
|
|
17
|
+
* Каталоги под `docs/specs`, доменами не являющиеся: шаблон содержит образцы с
|
|
18
|
+
* плейсхолдерами, и обязательных разделов у них нет.
|
|
19
|
+
*/
|
|
20
|
+
const NOT_DOMAINS = ['_template'];
|
|
21
|
+
/**
|
|
22
|
+
* Где ищутся тесты. Берётся из настройки дерева, а не из кода: зашитые здесь корни молча не
|
|
23
|
+
* находили ни одного теста у дерева, которое держит код иначе, — и каждый сценарий выглядел
|
|
24
|
+
* непокрытым, притом что тест на него был.
|
|
25
|
+
*/
|
|
26
|
+
const TEST_ROOTS = CONFIG.sourceRoots;
|
|
27
|
+
/** Где ищется вызов символа из привязки. */
|
|
28
|
+
const SOURCE_ROOTS = [...CONFIG.sourceRoots, ...(CONFIG.schemaFile ? [CONFIG.schemaFile.split('/')[0]] : [])];
|
|
29
|
+
const SKIPPED_DIRS = CONFIG.skippedDirs;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `### SC-BK-03 — заявка на занятые даты`
|
|
33
|
+
*
|
|
34
|
+
* Номер принимается от одной цифры до трёх. Заголовок, не подошедший под шаблон, сценария не
|
|
35
|
+
* заводит и отказа не даёт: дерево, пронумеровавшее сценарии с единицы, теряло бы первые
|
|
36
|
+
* девять из них молча — ни в покрытии, ни в долгах, при зелёной сверке.
|
|
37
|
+
*/
|
|
38
|
+
const SCENARIO_HEADING = /^###\s+(SC-([A-Z]{2,4})-(\d{1,3}))\s+—\s+(.+?)\s*$/;
|
|
39
|
+
/** Отметка осознанно непокрытого сценария; причина обязательна */
|
|
40
|
+
const UNCOVERED = /^Не покрыто:\s*\S/;
|
|
41
|
+
/** Тест есть, но проверяет не всё обещанное или идёт другим путём */
|
|
42
|
+
const PARTIAL = /^Покрытие:\s*частичное\s*—\s*\S/;
|
|
43
|
+
/** Упоминание сценария в заголовке теста; номер той же длины, что и в заголовке сценария */
|
|
44
|
+
const SCENARIO_REFERENCE = /\bSC-[A-Z]{2,4}-\d{1,3}\b/g;
|
|
45
|
+
/** Строка обещания сценария; её продолжения идут с отступом */
|
|
46
|
+
const PROMISE = /^Тогда\s+\S/;
|
|
47
|
+
/**
|
|
48
|
+
* Человек перед экраном и его восприятие. Границы слова не ставятся: `\b` в JavaScript
|
|
49
|
+
* считает буквой только латиницу, и `\bгость\b` не совпал бы ни разу.
|
|
50
|
+
*/
|
|
51
|
+
const ACTOR = /(гост[ьяию]|владел(?:ец|ьца|ьцу|ьцем)|сотрудник\w*|оператор\w*|пользовател\w+)/i;
|
|
52
|
+
const PERCEIVES = /(вид(?:ит|ят|но)|чита(?:ет|ют)|смотр(?:ит|ят))/i;
|
|
53
|
+
/** Сквозные тесты: только они идут тем же путём, что пользователь */
|
|
54
|
+
const E2E_ROOTS = CONFIG.e2eRoots;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Якорь правила: `путь/к/файлу.ts:символ` в обратных кавычках. Расширение до восьми
|
|
58
|
+
* букв — иначе `schema.prisma` не считается путём, и правило про умолчание колонки
|
|
59
|
+
* выглядит как правило без якоря. Заглавные и десять букв нужны ради `api.Dockerfile`:
|
|
60
|
+
* без них правило про режим исполнения образа считалось правилом с пустой привязкой,
|
|
61
|
+
* а привязать его больше не к чему — режим объявлен ровно там.
|
|
62
|
+
*
|
|
63
|
+
* Символ — любая буква, а не только латинская: тексты, которые исполняет модель, написаны
|
|
64
|
+
* своим языком, и латиницей в них называется ровно то, что утверждения не держит — имя поля
|
|
65
|
+
* шапки, имя инструмента. Привязанное к имени поля утверждение остаётся зелёным, когда текст
|
|
66
|
+
* переписан целиком. Алфавит не перечисляется диапазонами: перечисленные молча не покрывают
|
|
67
|
+
* соседнего, и промах выглядит отсутствием привязки. Путь при этом остаётся латинским — он
|
|
68
|
+
* адрес в дереве, а не слово текста.
|
|
69
|
+
*/
|
|
70
|
+
const ANCHOR = /`([\w./-]+\.[A-Za-z]{2,10}):(\p{L}[\p{L}\p{N}_-]*|_[\w-]*)`/gu;
|
|
71
|
+
/**
|
|
72
|
+
* Явный вердикт вместо якоря: статья, которой в дереве исполняться негде. Так бывает
|
|
73
|
+
* законно — правило говорит о службе, которой дерево не держит, или о движении человека,
|
|
74
|
+
* до которого проверке не дотянуться: кнопку слияния нажимают в браузере, где хуков нет
|
|
75
|
+
* вовсе. Якорь такой статье можно поставить только в файл, который её не исполняет, —
|
|
76
|
+
* проверка примет, а читателю совратёт.
|
|
77
|
+
*
|
|
78
|
+
* Принимается вердикт с причиной, а не одно слово: пустой он становится способом закрыть
|
|
79
|
+
* любую строку, и таблица за месяц превращается в список отговорок. Порог длины — та же
|
|
80
|
+
* мера, что у обхода гарда документов: причина короче его причиной не считается.
|
|
81
|
+
*
|
|
82
|
+
* Конец слова ищется отрицательным просмотром, а не `\b`: границей слова JavaScript знает
|
|
83
|
+
* только латиницу, и после кириллической буквы её нет вовсе — вердикт не опознавался ни
|
|
84
|
+
* разу.
|
|
85
|
+
*/
|
|
86
|
+
const VERDICT = /^\s*(?:\*\*)?Не (?:исполняется|применимо|проверяется)(?![\p{L}\p{N}_])/u;
|
|
87
|
+
const VERDICT_MIN = 40;
|
|
88
|
+
/** Строка шапки, объявляющая либы, чьи процедуры домен обслуживает */
|
|
89
|
+
const PROCEDURE_ROOTS = /^\*\*Процедуры:\*\*\s*(.+)$/;
|
|
90
|
+
const BACKTICKED = /`([^`]+)`/g;
|
|
91
|
+
|
|
92
|
+
const REQUIRED_HEADINGS = [
|
|
93
|
+
'## Зачем',
|
|
94
|
+
'## Терминология',
|
|
95
|
+
'### Как это называется в интерфейсе',
|
|
96
|
+
'## Правила',
|
|
97
|
+
'## Что не входит',
|
|
98
|
+
'## Контракт',
|
|
99
|
+
'### Коды отказов',
|
|
100
|
+
'## Данные',
|
|
101
|
+
'## Экраны и состояния',
|
|
102
|
+
'## Сквозные требования',
|
|
103
|
+
'### Локали',
|
|
104
|
+
'### SEO',
|
|
105
|
+
'### Мобильная раскладка',
|
|
106
|
+
'### Мультиобъектность',
|
|
107
|
+
'## Решения',
|
|
108
|
+
'## Открытые вопросы',
|
|
109
|
+
'## История изменений',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const problems = [];
|
|
113
|
+
const report = (where, message) => problems.push(`${where}: ${message}`);
|
|
114
|
+
|
|
115
|
+
function walk(dir, accept) {
|
|
116
|
+
const found = [];
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = readdirSync(join(ROOT, dir), { withFileTypes: true });
|
|
120
|
+
} catch {
|
|
121
|
+
return found;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const path = `${dir}/${entry.name}`;
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
if (!SKIPPED_DIRS.includes(entry.name)) {
|
|
128
|
+
found.push(...walk(path, accept));
|
|
129
|
+
}
|
|
130
|
+
} else if (accept(entry.name)) {
|
|
131
|
+
found.push(path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const read = (path) => readFileSync(join(ROOT, path), 'utf8');
|
|
139
|
+
const exists = (path) => existsSync(join(ROOT, path));
|
|
140
|
+
|
|
141
|
+
/** Директории доменов: `docs/specs/<домен>`, кроме шаблона. */
|
|
142
|
+
function collectDomains() {
|
|
143
|
+
try {
|
|
144
|
+
return readdirSync(join(ROOT, SPECS_DIR), { withFileTypes: true })
|
|
145
|
+
.filter((entry) => entry.isDirectory() && !NOT_DOMAINS.includes(entry.name))
|
|
146
|
+
.map((entry) => entry.name);
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Строки раздела: от его заголовка до следующего заголовка того же или более
|
|
154
|
+
* высокого уровня. Подразделы в раздел входят — «Коды отказов» разбираются
|
|
155
|
+
* отдельно, но остаются частью «Контракта».
|
|
156
|
+
*/
|
|
157
|
+
function sectionOf(text, heading) {
|
|
158
|
+
const level = heading.match(/^#+/)[0].length;
|
|
159
|
+
const lines = text.split('\n');
|
|
160
|
+
const start = lines.findIndex((line) => line.trimEnd() === heading);
|
|
161
|
+
if (start < 0) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const rest = lines.slice(start + 1);
|
|
165
|
+
const end = rest.findIndex((line) => {
|
|
166
|
+
const marks = line.match(/^(#+)\s/);
|
|
167
|
+
|
|
168
|
+
return marks && marks[1].length <= level;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return end < 0 ? rest : rest.slice(0, end);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Пункты списка верхнего уровня вместе с их продолжениями. */
|
|
175
|
+
function bulletsOf(lines) {
|
|
176
|
+
const bullets = [];
|
|
177
|
+
for (const [index, line] of lines.entries()) {
|
|
178
|
+
if (/^-\s+\S/.test(line)) {
|
|
179
|
+
bullets.push({ line: index, text: line });
|
|
180
|
+
} else if (bullets.length && /^\s+\S/.test(line)) {
|
|
181
|
+
bullets[bullets.length - 1].text += ` ${line.trim()}`;
|
|
182
|
+
} else if (!line.trim()) {
|
|
183
|
+
continue;
|
|
184
|
+
} else if (/^[#|]/.test(line)) {
|
|
185
|
+
// таблица или заголовок — список кончился
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return bullets;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export {
|
|
194
|
+
SPECS_DIR,
|
|
195
|
+
CONSTITUTION_DIR,
|
|
196
|
+
NOT_DOMAINS,
|
|
197
|
+
TEST_ROOTS,
|
|
198
|
+
SOURCE_ROOTS,
|
|
199
|
+
SKIPPED_DIRS,
|
|
200
|
+
SCENARIO_HEADING,
|
|
201
|
+
UNCOVERED,
|
|
202
|
+
PARTIAL,
|
|
203
|
+
SCENARIO_REFERENCE,
|
|
204
|
+
PROMISE,
|
|
205
|
+
ACTOR,
|
|
206
|
+
PERCEIVES,
|
|
207
|
+
E2E_ROOTS,
|
|
208
|
+
ANCHOR,
|
|
209
|
+
VERDICT,
|
|
210
|
+
VERDICT_MIN,
|
|
211
|
+
PROCEDURE_ROOTS,
|
|
212
|
+
BACKTICKED,
|
|
213
|
+
REQUIRED_HEADINGS,
|
|
214
|
+
problems,
|
|
215
|
+
report,
|
|
216
|
+
walk,
|
|
217
|
+
read,
|
|
218
|
+
exists,
|
|
219
|
+
collectDomains,
|
|
220
|
+
sectionOf,
|
|
221
|
+
bulletsOf,
|
|
222
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Контракт спека против того, что объявлено в коде: таблица процедур — против декораторов,
|
|
3
|
+
* коды отказов — против точек броска.
|
|
4
|
+
*/
|
|
5
|
+
import { BACKTICKED, PROCEDURE_ROOTS, bulletsOf, read, report, sectionOf, walk } from './spec-common.mjs';
|
|
6
|
+
|
|
7
|
+
// ── 2. Контракт против декораторов ────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/** Корни либ, чьи процедуры домен обслуживает; объявлены в шапке спека. */
|
|
10
|
+
function procedureRootsOf(text) {
|
|
11
|
+
const line = text.split('\n').find((candidate) => PROCEDURE_ROOTS.test(candidate));
|
|
12
|
+
if (!line) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const value = line.match(PROCEDURE_ROOTS)[1];
|
|
16
|
+
if (/^\s*нет\s*$/i.test(value.replace(/[`.]/g, ''))) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [...value.matchAll(BACKTICKED)].map(([, path]) => path);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Что объявлено в самих процедурах: метод контракта и право на него. */
|
|
24
|
+
function declaredProcedures(roots) {
|
|
25
|
+
const found = new Map();
|
|
26
|
+
for (const root of roots) {
|
|
27
|
+
for (const file of walk(root, (name) => name.endsWith('.procedure.ts'))) {
|
|
28
|
+
const text = read(file);
|
|
29
|
+
const method = text.match(/\.method\.([A-Za-z_]\w*)/);
|
|
30
|
+
if (!method) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const service = text.match(/typeof\s+(\w+)\.method\./);
|
|
34
|
+
const required = text.match(/@RequiresPermission\(\s*'([^']+)'/);
|
|
35
|
+
const isPublic = /@PublicProcedure\(/.test(text);
|
|
36
|
+
found.set(method[1].toLowerCase(), {
|
|
37
|
+
file,
|
|
38
|
+
method: method[1],
|
|
39
|
+
service: service ? service[1] : '',
|
|
40
|
+
permission: required ? required[1] : isPublic ? 'публично' : '',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Строки таблицы «Контракта»: первая ячейка — процедура, вторая — право. */
|
|
49
|
+
function contractRows(text) {
|
|
50
|
+
const rows = [];
|
|
51
|
+
for (const [index, line] of sectionOf(text, '## Контракт').entries()) {
|
|
52
|
+
if (!line.startsWith('|') || /^\|[\s:|-]+\|$/.test(line)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const cells = line
|
|
56
|
+
.split('|')
|
|
57
|
+
.slice(1, -1)
|
|
58
|
+
.map((cell) => cell.trim());
|
|
59
|
+
if (cells.length < 2) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const name = (cells[0].match(/`([^`]+)`/) || [])[1];
|
|
63
|
+
if (!name) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const permission = (cells[1].match(/`([^`]+)`/) || [])[1] || cells[1];
|
|
67
|
+
rows.push({ line: index, name, permission: permission.trim(), short: name.split('.').pop() });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function checkContract(file, text, roots) {
|
|
74
|
+
if (roots === null) {
|
|
75
|
+
report(file, 'в шапке нет строки `**Процедуры:**` — нечем сверить таблицу «Контракта» с декораторами');
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const declared = declaredProcedures(roots);
|
|
80
|
+
const rows = contractRows(text);
|
|
81
|
+
const described = new Set();
|
|
82
|
+
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const found = declared.get(row.short.toLowerCase());
|
|
85
|
+
if (!found) {
|
|
86
|
+
report(file, `в «Контракте» есть \`${row.name}\`, но процедуры с таким методом в ${roots.join(', ')} нет`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
described.add(row.short.toLowerCase());
|
|
90
|
+
if (found.permission && row.permission !== found.permission) {
|
|
91
|
+
report(
|
|
92
|
+
file,
|
|
93
|
+
`право у \`${row.name}\` разошлось: в спеке «${row.permission}», ` + `в \`${found.file}\` объявлено «${found.permission}»`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const [key, found] of declared) {
|
|
99
|
+
if (!described.has(key)) {
|
|
100
|
+
report(
|
|
101
|
+
file,
|
|
102
|
+
`процедура \`${found.service}.${found.method}\` (${found.file}) домену принадлежит, ` + 'но в таблице «Контракта» её нет'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── 3. Коды отказов ───────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function checkRefusalCodes(file, text, roots) {
|
|
111
|
+
const section = sectionOf(text, '### Коды отказов');
|
|
112
|
+
const bullets = bulletsOf(section);
|
|
113
|
+
/**
|
|
114
|
+
* «Не применимо» — законный ответ и здесь. Домен, у которого есть процедуры,
|
|
115
|
+
* но нет ни одного `Code.X`, иначе не описывался вовсе: пустой раздел
|
|
116
|
+
* проверка отвергает, а любой выписанный код отвергает тем более — бросать
|
|
117
|
+
* его в домене некому. Так живёт проверка живости: отказ она подаёт кодом
|
|
118
|
+
* ответа, а не отказом процедуры.
|
|
119
|
+
*/
|
|
120
|
+
// Без `\b`: кириллица не входит в `\w`, и границы слова после «применимо» не возникает
|
|
121
|
+
const notApplicable = section.some((line) => /^Не применимо/.test(line.trim()));
|
|
122
|
+
if (!bullets.length) {
|
|
123
|
+
if (!notApplicable) {
|
|
124
|
+
report(file, 'в разделе `### Коды отказов` нет ни одного кода и нет ответа «Не применимо»');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!roots || !roots.length) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const sources = roots.flatMap((root) => walk(root, (name) => name.endsWith('.ts') && !name.endsWith('.spec.ts')));
|
|
133
|
+
const thrown = new Set();
|
|
134
|
+
for (const source of sources) {
|
|
135
|
+
for (const [, code] of read(source).matchAll(/\bCode\.([A-Za-z]\w*)/g)) {
|
|
136
|
+
thrown.add(code);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const bullet of bullets) {
|
|
141
|
+
const code = (bullet.text.match(/`([A-Za-z]\w*)`/) || [])[1];
|
|
142
|
+
if (!code) {
|
|
143
|
+
report(file, `в «Кодах отказов» строка без кода в кавычках: «${bullet.text.slice(0, 60)}…»`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!thrown.has(code)) {
|
|
147
|
+
report(file, `код отказа \`${code}\` в домене нигде не бросается — либо он не отсюда, либо путь его не даёт`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { checkContract, checkRefusalCodes, procedureRootsOf };
|