@rt-tools/agent-kit 0.8.3 → 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 +12 -0
- package/assets/checks/board.github.mjs +48 -1
- package/assets/checks/check-board.github.mjs +84 -1
- package/assets/checks/check-lib-layers.mjs +13 -524
- package/assets/checks/check-specs.mjs +11 -782
- 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 +17 -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/defaults/project.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -4
- 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/waiting-turn-guard.sh +42 -13
- package/assets/laws/delivery.md +7 -0
- package/assets/laws/work-conduct.md +9 -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/observability-record.md +9 -0
- package/assets/patterns/shared-code-new.md +2 -2
- package/assets/patterns/task-flow-close.md +7 -1
- package/assets/rules/git-workflow.azure.md +7 -0
- package/assets/rules/git-workflow.github.md +41 -0
- package/assets/rules/git-workflow.gitlab.md +7 -0
- package/assets/rules/lib-layers.md +4 -0
- package/assets/rules/lists.md +10 -10
- package/assets/rules/shared-code.md +1 -1
- package/assets/rules/task-flow.md +47 -7
- package/assets/rules/testing.md +31 -0
- package/assets/rules/typescript-conventions.md +7 -0
- package/assets/skills/agent-kit.md +36 -0
- package/assets/templates/proposal.md +21 -0
- 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 +152 -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 +2 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +7 -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.3.tgz +0 -0
|
@@ -13,16 +13,25 @@
|
|
|
13
13
|
* которого правило есть, но у чужого блока, — направление выбрано в сторону
|
|
14
14
|
* ложных пропусков, а не ложных отказов.
|
|
15
15
|
*
|
|
16
|
+
* Объявлением считается и то, что приехало подключённым пакетом: экран,
|
|
17
|
+
* собранный из готового, своих объявлений не держит вовсе. Читается ровно тот
|
|
18
|
+
* файл, который приложение назвало само, — обход односложный, и каталог
|
|
19
|
+
* зависимостей в корни исходников не попадает.
|
|
20
|
+
*
|
|
16
21
|
* Динамическое `[rtElem]` не считается: имя там известно только в рантайме.
|
|
17
22
|
*
|
|
18
23
|
* Накопленное к моменту заведения проверки лежит в tools/styles-allowlist.json
|
|
19
24
|
* и отказом не считается: гейт падает на НОВОМ классе без правила, а старое
|
|
20
|
-
* остаётся видимым числом в сводке.
|
|
25
|
+
* остаётся видимым числом в сводке. Строка списка опознаётся по имени класса:
|
|
26
|
+
* перечень файлов при нём меняется от каждой правки разметки, и сверенный
|
|
27
|
+
* целиком он делал бы прежнюю строку лишней, а тот же самый долг — новым
|
|
28
|
+
* расхождением.
|
|
21
29
|
*
|
|
22
30
|
* Ненулевой код возврата и перечень расхождений.
|
|
23
31
|
*/
|
|
24
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
25
|
-
import {
|
|
32
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
34
|
+
import { basename, dirname, join } from 'node:path';
|
|
26
35
|
|
|
27
36
|
import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
28
37
|
|
|
@@ -33,11 +42,28 @@ const SKIPPED_DIRS = CONFIG.skippedDirs;
|
|
|
33
42
|
const ELEM_RE = /rtElem="([a-z0-9-]+)"/g;
|
|
34
43
|
/** Объявление элемента: и вложенное `&__item`, и полное `.<блок>__item` */
|
|
35
44
|
const RULE_RE = /__([a-z0-9-]+)/g;
|
|
45
|
+
/** Голова вложенности: строка, с которой начинается блок элемента */
|
|
46
|
+
const HEAD_RE = /^&__([a-z0-9-]+)/;
|
|
47
|
+
/** Колено вложенности: `&-<хвост>` внутри блока элемента дописывает имя, а не заводит своё */
|
|
48
|
+
const TAIL_RE = /^&-([a-z0-9-]+)/;
|
|
49
|
+
/** Подключение в файле стилей: `@use` и `@forward` берутся одним разбором */
|
|
50
|
+
const USE_RE = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
|
|
51
|
+
/** Строка списка известного: имя класса и перечень файлов при нём */
|
|
52
|
+
const KEY_RE = /^elem (\S+) @ (.*)$/;
|
|
36
53
|
|
|
37
54
|
const allowlist = JSON.parse(readFileSync(join(ROOT, ALLOWLIST), 'utf8'));
|
|
38
55
|
const known = new Set([...(allowlist.accepted ?? []), ...(allowlist.debt ?? [])]);
|
|
39
56
|
const debt = new Set(allowlist.debt ?? []);
|
|
40
57
|
|
|
58
|
+
/** Строки списка по имени класса: перечень файлов в ключ входит, но сверяется отдельно */
|
|
59
|
+
const knownByName = new Map();
|
|
60
|
+
for (const key of known) {
|
|
61
|
+
const parsed = KEY_RE.exec(key);
|
|
62
|
+
if (parsed) {
|
|
63
|
+
knownByName.set(parsed[1], { key, files: new Set(parsed[2].split(', ').filter(Boolean)) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
function collectFiles(dir, extension) {
|
|
42
68
|
const files = [];
|
|
43
69
|
for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
|
@@ -54,15 +80,121 @@ function collectFiles(dir, extension) {
|
|
|
54
80
|
return files;
|
|
55
81
|
}
|
|
56
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Имя пакета и путь внутри него. Относительный и абсолютный спецификаторы пакетом не бывают;
|
|
85
|
+
* приставки `pkg:` и `~` снимаются — ими зовут тот же пакет разные сборщики.
|
|
86
|
+
*/
|
|
87
|
+
function packageOf(specifier) {
|
|
88
|
+
const clean = specifier.replace(/^pkg:/, '').replace(/^~/, '');
|
|
89
|
+
if (clean === '' || clean.startsWith('.') || clean.startsWith('/')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const parts = clean.split('/');
|
|
93
|
+
const name = clean.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
94
|
+
if (clean.startsWith('@') && parts.length < 2) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { name, rest: clean.slice(name.length).replace(/^\//, '') };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Файл пакета, названный подключением. Каталог ищется разрешением модуля от того места, где
|
|
103
|
+
* подключение написано: пакет подпроекта в корне дерева не лежит вовсе, а менеджер вправе
|
|
104
|
+
* держать рядом несколько версий сразу. Не нашлось — `null`: дерево без этого пакета получает
|
|
105
|
+
* сверку своих объявлений, а не отказ чтения.
|
|
106
|
+
*/
|
|
107
|
+
function fileOfPackage(from, { name, rest }) {
|
|
108
|
+
let manifest;
|
|
109
|
+
try {
|
|
110
|
+
manifest = createRequire(join(ROOT, from, 'package.json')).resolve(`${name}/package.json`);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const base = dirname(manifest);
|
|
116
|
+
const tail = rest === '' ? 'index' : rest;
|
|
117
|
+
const candidates = [
|
|
118
|
+
join(base, tail),
|
|
119
|
+
join(base, `${tail}.scss`),
|
|
120
|
+
join(base, dirname(tail), `_${basename(tail)}.scss`),
|
|
121
|
+
join(base, tail, '_index.scss'),
|
|
122
|
+
join(base, tail, 'index.scss'),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return candidates.find((path) => existsSync(path) && statSync(path).isFile()) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Имена элементов, объявленных файлом стилей. Имя собирается из вложенности: `&__head { &-icon }`
|
|
130
|
+
* объявляет `head-icon`, и колен у него бывает сколько угодно. Читая только то, что стоит после
|
|
131
|
+
* `__` целиком, проверка числила долгом исправную вёрстку — правило работает, класс красит, а
|
|
132
|
+
* снять его значило бы сломать экран.
|
|
133
|
+
*
|
|
134
|
+
* Хвост без головы именем не становится: `&-<хвост>`, стоящий вне блока элемента, принадлежит
|
|
135
|
+
* чужому селектору, и приписать его было бы выдумыванием объявления.
|
|
136
|
+
*/
|
|
137
|
+
function elementNames(text) {
|
|
138
|
+
const names = new Set();
|
|
139
|
+
const stack = [];
|
|
140
|
+
for (const raw of text.split('\n')) {
|
|
141
|
+
const line = raw.trim();
|
|
142
|
+
const parent = stack.length ? stack[stack.length - 1] : '';
|
|
143
|
+
const head = HEAD_RE.exec(line);
|
|
144
|
+
const tail = TAIL_RE.exec(line);
|
|
145
|
+
let current = parent;
|
|
146
|
+
|
|
147
|
+
if (head) {
|
|
148
|
+
current = head[1];
|
|
149
|
+
} else if (tail && parent) {
|
|
150
|
+
current = `${parent}-${tail[1]}`;
|
|
151
|
+
names.add(current);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const match of line.matchAll(RULE_RE)) {
|
|
155
|
+
names.add(match[1]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (let index = 0; index < (line.match(/{/g) ?? []).length; index += 1) {
|
|
159
|
+
stack.push(current);
|
|
160
|
+
}
|
|
161
|
+
for (let index = 0; index < (line.match(/}/g) ?? []).length && stack.length; index += 1) {
|
|
162
|
+
stack.pop();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return names;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Объявления из пакетов, подключённых самим приложением. Читается ровно названный файл:
|
|
171
|
+
* подключения внутри него не разбираются — объявленным считается то, что приложение назвало.
|
|
172
|
+
*/
|
|
173
|
+
function declarationsFromPackages(styleFiles) {
|
|
174
|
+
const names = new Set();
|
|
175
|
+
for (const path of styleFiles) {
|
|
176
|
+
for (const [, specifier] of readFileSync(join(ROOT, path), 'utf8').matchAll(USE_RE)) {
|
|
177
|
+
const parsed = packageOf(specifier);
|
|
178
|
+
const file = parsed && fileOfPackage(dirname(path), parsed);
|
|
179
|
+
if (!file) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
elementNames(readFileSync(file, 'utf8')).forEach((name) => names.add(name));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return names;
|
|
187
|
+
}
|
|
188
|
+
|
|
57
189
|
const declared = new Set();
|
|
58
190
|
const usedIn = new Map();
|
|
59
191
|
|
|
60
192
|
for (const root of SOURCE_ROOTS) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
193
|
+
const styleFiles = collectFiles(root, '.scss');
|
|
194
|
+
for (const path of styleFiles) {
|
|
195
|
+
elementNames(readFileSync(join(ROOT, path), 'utf8')).forEach((name) => declared.add(name));
|
|
65
196
|
}
|
|
197
|
+
declarationsFromPackages(styleFiles).forEach((name) => declared.add(name));
|
|
66
198
|
|
|
67
199
|
for (const path of collectFiles(root, '.html')) {
|
|
68
200
|
for (const match of readFileSync(join(ROOT, path), 'utf8').matchAll(ELEM_RE)) {
|
|
@@ -82,21 +214,58 @@ for (const [name, files] of [...usedIn].sort(([first], [second]) => first.locale
|
|
|
82
214
|
continue;
|
|
83
215
|
}
|
|
84
216
|
const where = [...files].sort().join(', ');
|
|
85
|
-
findings.push({
|
|
217
|
+
findings.push({
|
|
218
|
+
name,
|
|
219
|
+
files: new Set(files),
|
|
220
|
+
key: `elem ${name} @ ${where}`,
|
|
221
|
+
text: `rtElem="${name}" — правила нет ни в одном файле стилей: ${where}`,
|
|
222
|
+
});
|
|
86
223
|
}
|
|
87
224
|
|
|
88
|
-
const fresh = findings.filter((finding) => !known.has(finding.key));
|
|
89
|
-
const staleKeys = [...known].filter((key) => !findings.some((finding) => finding.key === key));
|
|
90
|
-
|
|
91
225
|
if (process.argv.includes('--baseline')) {
|
|
92
226
|
console.log(JSON.stringify({ ...allowlist, debt: findings.map((finding) => finding.key).sort() }, null, 4));
|
|
93
227
|
process.exit(0);
|
|
94
228
|
}
|
|
95
229
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Чем перечень файлов у находки разошёлся со строкой списка: что добавилось и что ушло. Рост —
|
|
232
|
+
* расхождение, сокращение — починка; одинаково их читать нельзя, а сверять ключ целиком значило
|
|
233
|
+
* бы объявлять тот же долг новым.
|
|
234
|
+
*/
|
|
235
|
+
function changedFiles(finding, line) {
|
|
236
|
+
return {
|
|
237
|
+
added: [...finding.files].filter((path) => !line.files.has(path)).sort(),
|
|
238
|
+
gone: [...line.files].filter((path) => !finding.files.has(path)).sort(),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const problems = [];
|
|
243
|
+
const notes = [];
|
|
244
|
+
const matchedKeys = new Set();
|
|
245
|
+
|
|
246
|
+
for (const finding of findings) {
|
|
247
|
+
const line = knownByName.get(finding.name);
|
|
248
|
+
if (!line) {
|
|
249
|
+
problems.push(finding.text);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
matchedKeys.add(line.key);
|
|
253
|
+
|
|
254
|
+
const { added, gone } = changedFiles(finding, line);
|
|
255
|
+
if (added.length > 0) {
|
|
256
|
+
problems.push(`elem ${finding.name}: долг разросся — класс появился ещё в ${added.join(', ')}; снять его оттуда`);
|
|
257
|
+
} else if (gone.length > 0) {
|
|
258
|
+
notes.push(
|
|
259
|
+
`elem ${finding.name}: долг сократился — класса больше нет в ${gone.join(', ')}; перечень в строке списка можно поправить`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const key of known) {
|
|
265
|
+
if (!matchedKeys.has(key)) {
|
|
266
|
+
problems.push(`${key}: значится в ${ALLOWLIST}, но класс уже подкреплён правилом — строку убрать`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
100
269
|
|
|
101
270
|
if (problems.length > 0) {
|
|
102
271
|
console.error(`check-styles: расхождений ${problems.length}\n`);
|
|
@@ -104,6 +273,7 @@ if (problems.length > 0) {
|
|
|
104
273
|
process.exit(1);
|
|
105
274
|
}
|
|
106
275
|
|
|
276
|
+
notes.forEach((note) => console.log(` ${note}`));
|
|
107
277
|
console.log(
|
|
108
278
|
`check-styles: классов без правила ${findings.length}, из них принято ${findings.length - debt.size}, долг ${debt.size} — новых нет`
|
|
109
279
|
);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Границы и теги: где либа описана как источник, чей список зависимостей обязан оставаться
|
|
3
|
+
* пустым и что видно основанию семейства. Все три предмета читают одни и те же файлы
|
|
4
|
+
* `eslint/boundaries/domains/`, и врозь их не разложить.
|
|
5
|
+
*
|
|
6
|
+
* Имя не начинается с `check-`: перебором таких имён умолчание пакета собирает набор гейта
|
|
7
|
+
* пуша, и помощник с ним гейт стал бы гонять как отдельную проверку.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { BOUNDARIES_DIR, FAMILIES, isDir, projectTag, report } from './lib-common.mjs';
|
|
13
|
+
import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Либа должна быть описана ровно одним `sourceTag` и ровно в одном файле границ:
|
|
17
|
+
* описанная дважды либа означает два разных allowlist'а на неё, и какой победит —
|
|
18
|
+
* вопрос порядка сборки. В чужих allowlist'ах тег, наоборот, встречается сколько
|
|
19
|
+
* угодно раз: это перечисление тех, кому либа видна.
|
|
20
|
+
*/
|
|
21
|
+
function checkBoundaries(libs) {
|
|
22
|
+
if (!isDir(BOUNDARIES_DIR)) {
|
|
23
|
+
report(BOUNDARIES_DIR, 'каталога с файлами границ нет');
|
|
24
|
+
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const files = readdirSync(join(ROOT, BOUNDARIES_DIR)).filter((name) => name.endsWith('.config.mjs'));
|
|
29
|
+
const sources = files.map((name) => ({ name, text: readFileSync(join(ROOT, BOUNDARIES_DIR, name), 'utf8') }));
|
|
30
|
+
|
|
31
|
+
for (const libPath of libs) {
|
|
32
|
+
const tag = projectTag(libPath);
|
|
33
|
+
const declaration = `sourceTag: '${tag}'`;
|
|
34
|
+
const owners = sources.flatMap(({ name, text }) => {
|
|
35
|
+
const count = text.split(declaration).length - 1;
|
|
36
|
+
|
|
37
|
+
return count > 0 ? Array.from({ length: count }, () => name) : [];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (owners.length === 0) {
|
|
41
|
+
report(libPath, `тег ${tag} не описан ни в одном файле ${BOUNDARIES_DIR}/`);
|
|
42
|
+
} else if (owners.length > 1) {
|
|
43
|
+
report(libPath, `тег ${tag} описан ${owners.length} раза: ${owners.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Либы, у которых список зависимостей обязан оставаться пустым. Барель такой либы
|
|
50
|
+
* уезжает в бандл бэкенда, а сборка API идёт без tree-shaking: любая зависимость
|
|
51
|
+
* расходится оттуда по всему графу.
|
|
52
|
+
*
|
|
53
|
+
* Теги называет дерево: имя тега — его собственное слово, и зашитое здесь требовало бы описания
|
|
54
|
+
* границ под либу, которой в дереве нет вовсе. Пусто — таких либ дерево не держит.
|
|
55
|
+
*/
|
|
56
|
+
const NO_DEPENDENCY_TAGS = CONFIG.noDependencyTags;
|
|
57
|
+
|
|
58
|
+
function checkNoDependencyLibs() {
|
|
59
|
+
if (!isDir(BOUNDARIES_DIR)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const files = readdirSync(join(ROOT, BOUNDARIES_DIR)).filter((name) => name.endsWith('.config.mjs'));
|
|
64
|
+
|
|
65
|
+
for (const tag of NO_DEPENDENCY_TAGS) {
|
|
66
|
+
const declaration = `sourceTag: '${tag}'`;
|
|
67
|
+
let described = false;
|
|
68
|
+
|
|
69
|
+
for (const name of files) {
|
|
70
|
+
const text = readFileSync(join(ROOT, BOUNDARIES_DIR, name), 'utf8');
|
|
71
|
+
const declaredAt = text.indexOf(declaration);
|
|
72
|
+
if (declaredAt === -1) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
described = true;
|
|
76
|
+
|
|
77
|
+
const rest = text.slice(declaredAt);
|
|
78
|
+
const listAt = rest.indexOf('onlyDependOnLibsWithTags:');
|
|
79
|
+
const opensAt = rest.indexOf('[', listAt);
|
|
80
|
+
const closesAt = rest.indexOf(']', opensAt);
|
|
81
|
+
if (listAt === -1 || opensAt === -1 || closesAt === -1) {
|
|
82
|
+
report(`${BOUNDARIES_DIR}/${name}`, `у ${tag} не найден список зависимостей`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dependencies = rest.slice(opensAt + 1, closesAt).trim();
|
|
87
|
+
if (dependencies.length > 0) {
|
|
88
|
+
report(`${BOUNDARIES_DIR}/${name}`, `${tag} ни от кого не зависит, а его список непуст: ${dependencies}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!described) {
|
|
93
|
+
report(BOUNDARIES_DIR, `тег ${tag} не описан ни в одном файле границ`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Основание семейства видит `common/util` и слои `util` общих доменов своей
|
|
100
|
+
* семьи. Всё сверх этого названо строкой здесь: основание зовут все домены, и
|
|
101
|
+
* любая его зависимость становится общей для всей семьи.
|
|
102
|
+
*/
|
|
103
|
+
const CORE_EXCEPTIONS = {
|
|
104
|
+
// Транспорт Connect в admin-transport.provider.ts: дескриптор логина и токен транспорта
|
|
105
|
+
'scope:admin-core': ['scope:common-proto', 'scope:common-connect'],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** `scope:<семья>-common-<домен>-util` — слой `util` общего домена той же семьи */
|
|
109
|
+
const isFamilyCommonUtil = (tag, family) => new RegExp(`^scope:${family}-common-[a-z0-9-]+-util$`).test(tag);
|
|
110
|
+
|
|
111
|
+
async function checkCoreLibs() {
|
|
112
|
+
// Свод границ есть не у всякого дерева с доменной сеткой: он собирается отдельным файлом, и
|
|
113
|
+
// пока его отсутствие не обрабатывалось, проверка падала на импорте — то есть отвечала
|
|
114
|
+
// поломкой на дерево, где границы объявлены иначе.
|
|
115
|
+
const boundaries = join(ROOT, 'eslint/boundaries/index.mjs');
|
|
116
|
+
if (!existsSync(boundaries)) {
|
|
117
|
+
console.log('пропущено: свода границ eslint/boundaries/index.mjs в дереве нет');
|
|
118
|
+
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const { allBoundaries } = await import(boundaries);
|
|
122
|
+
|
|
123
|
+
for (const family of FAMILIES) {
|
|
124
|
+
const tag = `scope:${family}-core`;
|
|
125
|
+
const rule = allBoundaries.find((entry) => entry.sourceTag === tag);
|
|
126
|
+
if (!rule) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const allowed = CORE_EXCEPTIONS[tag] ?? [];
|
|
131
|
+
for (const dependency of rule.onlyDependOnLibsWithTags ?? []) {
|
|
132
|
+
if (dependency === 'scope:common-util' || isFamilyCommonUtil(dependency, family) || allowed.includes(dependency)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
report(
|
|
136
|
+
`${BOUNDARIES_DIR}/${family}-core.config.mjs`,
|
|
137
|
+
`${tag} видит ${dependency}: основанию семейства доступны только scope:common-util и scope:${family}-common-*-util`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { checkBoundaries, checkCoreLibs, checkNoDependencyLibs };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Общее для всех предметов сверки раскладки либ: состав слоёв каждой формы домена, чтение
|
|
3
|
+
* дерева, список принятых долгов и то, как из пути либы получаются её имя, тег и алиас.
|
|
4
|
+
*
|
|
5
|
+
* Модуль назван не `check-*`, и это не украшение: умолчание пакета собирает набор гейта пуша
|
|
6
|
+
* перебором имён `check-<что>.mjs` в каталоге проверок, и помощник с таким именем гейт стал бы
|
|
7
|
+
* гонять как отдельную проверку.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { CONFIG, ROOT, readAllowlist } from './rt-kit-checks.config.mjs';
|
|
13
|
+
|
|
14
|
+
const FAMILIES = CONFIG.families;
|
|
15
|
+
/**
|
|
16
|
+
* Корень либ и семья бэкенда берутся из настройки дерева, а не из кода: у дерева, которое
|
|
17
|
+
* держит либы под другим именем, обход шёл мимо кода и проверка зеленела на пустом каталоге —
|
|
18
|
+
* то есть отвечала «нарушений нет» там, где она не смотрела вовсе.
|
|
19
|
+
*/
|
|
20
|
+
const LIBS_ROOT = CONFIG.libsRoot;
|
|
21
|
+
const API_FAMILY = CONFIG.apiFamily;
|
|
22
|
+
/**
|
|
23
|
+
* Приставка селекторов, обязательная либам фронта. Слово принадлежит дереву целиком; пусто —
|
|
24
|
+
* приставка здесь ничего не значит, и проверка о ней молчит.
|
|
25
|
+
*/
|
|
26
|
+
const LIB_PREFIX = CONFIG.libPrefix;
|
|
27
|
+
/**
|
|
28
|
+
* Имена, под которыми в этом дереве лежит барель. Их несколько: публикуемый пакет зовёт свой
|
|
29
|
+
* `public-api.ts`, а либа доменной сетки — `index.ts`, и оба живут в одном дереве. Имя, не
|
|
30
|
+
* названное здесь, барелем не считается, и собственный файл в нём читается реэкспортом.
|
|
31
|
+
*/
|
|
32
|
+
const BARREL_FILES = CONFIG.barrelFiles;
|
|
33
|
+
const FLAT_LAYERS = ['api', 'data-access', 'ui', 'util'];
|
|
34
|
+
const FEATURE_DOMAIN_LAYERS = [...FLAT_LAYERS, 'feature', 'shell'].sort();
|
|
35
|
+
const COMMON_DOMAIN_LAYERS = [...FLAT_LAYERS, 'feature'].sort();
|
|
36
|
+
/** У бэкенда нет `ui` и `shell`: отдавать разметку и роутиться ему нечем */
|
|
37
|
+
const API_DOMAIN_LAYERS = ['api', 'data-access', 'feature', 'util'];
|
|
38
|
+
/** Где ищутся либы, оказавшиеся вне доменной сетки */
|
|
39
|
+
const LIB_ROOTS = [...FAMILIES, API_FAMILY].map((family) => `${LIBS_ROOT}/${family}`);
|
|
40
|
+
const REQUIRED_FILES = ['project.json', 'tsconfig.json', 'vitest.config.mts', 'src/index.ts'];
|
|
41
|
+
const BOUNDARIES_DIR = 'eslint/boundaries/domains';
|
|
42
|
+
|
|
43
|
+
const problems = [];
|
|
44
|
+
const report = (path, message) => problems.push(`${path}: ${message}`);
|
|
45
|
+
|
|
46
|
+
const readJson = (path) => JSON.parse(readFileSync(join(ROOT, path), 'utf8'));
|
|
47
|
+
const isDir = (path) => existsSync(join(ROOT, path)) && statSync(join(ROOT, path)).isDirectory();
|
|
48
|
+
const IGNORED_DIRS = ['src', 'node_modules'];
|
|
49
|
+
const dirsIn = (path) =>
|
|
50
|
+
isDir(path)
|
|
51
|
+
? readdirSync(join(ROOT, path), { withFileTypes: true })
|
|
52
|
+
.filter((entry) => entry.isDirectory() && !IGNORED_DIRS.includes(entry.name))
|
|
53
|
+
.map((entry) => entry.name)
|
|
54
|
+
.sort()
|
|
55
|
+
: [];
|
|
56
|
+
const isLib = (path) => existsSync(join(ROOT, path, 'project.json'));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Список принятых долгов. Читается помощником настроек, а не напрямую: файла в свежем дереве
|
|
60
|
+
* нет вовсе, и прямое чтение роняло проверку отказом «нет такого файла» — то есть первая же
|
|
61
|
+
* установка получала поломку вместо отчёта о том, что долгов нет.
|
|
62
|
+
*/
|
|
63
|
+
const allowlist = readAllowlist('lib-layers');
|
|
64
|
+
const pathsOf = (key) => (allowlist[key] ?? []).map((exception) => exception.path);
|
|
65
|
+
|
|
66
|
+
/** Паттерн `<корень>/x/*` покрывает и сам каталог `<корень>/x`: исключение снимается целиком */
|
|
67
|
+
const matches = (patterns, path) =>
|
|
68
|
+
patterns.some((pattern) => {
|
|
69
|
+
if (!pattern.endsWith('/*')) {
|
|
70
|
+
return path === pattern;
|
|
71
|
+
}
|
|
72
|
+
const prefix = pattern.slice(0, -2);
|
|
73
|
+
|
|
74
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Каталог, который доменом не является вовсе: основание семейства, общие либы, бэкенд */
|
|
78
|
+
const isNotDomain = (path) => matches(pathsOf('notDomains'), path);
|
|
79
|
+
|
|
80
|
+
/** Домен старой раскладки: не проверяется ни как домен, ни как набор либ — уезжает целиком */
|
|
81
|
+
const isLegacyDomain = (path) => matches(pathsOf('legacyDomains'), path);
|
|
82
|
+
|
|
83
|
+
/** Либа старой раскладки внутри живого домена: домен проверяется, сама либа — нет */
|
|
84
|
+
const isLegacyLib = (path) => matches(pathsOf('legacyLibs'), path);
|
|
85
|
+
|
|
86
|
+
/** Домен, которому один заполненный слой разрешён: причина названа в списке исключений */
|
|
87
|
+
const isSingleLayerDomain = (path) => matches(pathsOf('singleLayerDomains'), path);
|
|
88
|
+
|
|
89
|
+
/** Пути, которые обход либ обязан пропустить */
|
|
90
|
+
const isIgnoredLib = (path) => isNotDomain(path) || isLegacyDomain(path) || isLegacyLib(path);
|
|
91
|
+
|
|
92
|
+
const projectName = (libPath) => libPath.replace(/^libs\//, '').replaceAll('/', '-');
|
|
93
|
+
const projectTag = (libPath) => `scope:${projectName(libPath)}`;
|
|
94
|
+
const importAlias = (libPath) => `${CONFIG.importScope}/${libPath.slice(`${LIBS_ROOT}/`.length)}`;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Файлы либы, кроме бареля: барель есть у пустого слоя так же, как у заполненного, и
|
|
98
|
+
* пустой слой от заполненного по нему не отличить.
|
|
99
|
+
*/
|
|
100
|
+
function sourceCount(libPath) {
|
|
101
|
+
const inside = (dir) => {
|
|
102
|
+
let total = 0;
|
|
103
|
+
for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (!IGNORED_DIRS.includes(entry.name)) {
|
|
106
|
+
total += inside(`${dir}/${entry.name}`);
|
|
107
|
+
}
|
|
108
|
+
} else if (entry.name.endsWith('.ts')) {
|
|
109
|
+
total += 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return total;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return isDir(`${libPath}/src/lib`) ? inside(`${libPath}/src/lib`) : 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
FAMILIES,
|
|
121
|
+
LIBS_ROOT,
|
|
122
|
+
API_FAMILY,
|
|
123
|
+
LIB_PREFIX,
|
|
124
|
+
BARREL_FILES,
|
|
125
|
+
FLAT_LAYERS,
|
|
126
|
+
FEATURE_DOMAIN_LAYERS,
|
|
127
|
+
COMMON_DOMAIN_LAYERS,
|
|
128
|
+
API_DOMAIN_LAYERS,
|
|
129
|
+
LIB_ROOTS,
|
|
130
|
+
REQUIRED_FILES,
|
|
131
|
+
BOUNDARIES_DIR,
|
|
132
|
+
IGNORED_DIRS,
|
|
133
|
+
allowlist,
|
|
134
|
+
problems,
|
|
135
|
+
report,
|
|
136
|
+
readJson,
|
|
137
|
+
isDir,
|
|
138
|
+
dirsIn,
|
|
139
|
+
isLib,
|
|
140
|
+
isNotDomain,
|
|
141
|
+
isLegacyDomain,
|
|
142
|
+
isLegacyLib,
|
|
143
|
+
isSingleLayerDomain,
|
|
144
|
+
isIgnoredLib,
|
|
145
|
+
projectName,
|
|
146
|
+
projectTag,
|
|
147
|
+
importAlias,
|
|
148
|
+
sourceCount,
|
|
149
|
+
};
|