@rt-tools/agent-kit 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/assets/checks/check-push-gate.mjs +139 -0
- package/assets/checks/check-reuse.mjs +32 -98
- package/assets/checks/rt-kit-checks.config.mjs +19 -0
- package/assets/checks/signals/angular.json +19 -0
- package/assets/checks/signals/core.json +19 -0
- package/assets/checks/signals/store.json +14 -0
- package/assets/checks/signals/ui-kit-v2.json +48 -0
- package/assets/checks/signals/ui-kit.json +11 -0
- package/assets/checks/signals/utils.json +15 -0
- package/assets/checks/signals.mjs +71 -0
- package/assets/defaults/project.sh +9 -7
- package/assets/hooks/reuse-first-guard.sh +90 -1
- package/assets/patterns/git-workflow-commit.azure.md +18 -0
- package/assets/patterns/git-workflow-commit.github.md +18 -0
- package/assets/patterns/git-workflow-commit.gitlab.md +18 -0
- package/assets/patterns/reuse-first-extend.md +30 -0
- package/assets/patterns/spec-driven-domain.md +2 -2
- package/assets/rules/git-workflow.azure.md +15 -0
- package/assets/rules/git-workflow.github.md +15 -0
- package/assets/rules/git-workflow.gitlab.md +15 -0
- package/assets/rules/reuse-first.md +31 -13
- package/assets/skills/agent-kit.md +13 -8
- package/lib/assets.d.ts +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +2 -4
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +73 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +121 -0
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +84 -5
- package/lib/commands.js.map +1 -1
- package/lib/integrity.d.ts +25 -12
- package/lib/integrity.d.ts.map +1 -1
- package/lib/integrity.js +40 -18
- package/lib/integrity.js.map +1 -1
- package/lib/proposals.d.ts +9 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +11 -2
- package/lib/proposals.js.map +1 -1
- package/lib/retired.d.ts +30 -0
- package/lib/retired.d.ts.map +1 -0
- package/lib/retired.js +19 -0
- package/lib/retired.js.map +1 -0
- package/lib/sync.d.ts +45 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +44 -10
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.8.0.tgz +0 -0
- package/assets/laws/application/money.md +0 -41
- package/assets/laws/application/ownership.md +0 -32
- package/assets/patterns/ownership-scope-resolve.md +0 -69
- package/assets/patterns/ownership-session-procedure.md +0 -102
- package/assets/patterns/pricing-quote.md +0 -71
- package/assets/rules/ownership-scope.md +0 -66
- package/assets/rules/ownership-session.md +0 -124
- package/assets/rules/pricing.md +0 -68
- package/rt-tools-agent-kit-0.6.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -185,6 +185,19 @@ npx agent-kit init --laws access,delivery,verifiability
|
|
|
185
185
|
трогает. Имена законов — из `agent-kit list`, там же видно, что взято, что не выбрано и что
|
|
186
186
|
пропущено.
|
|
187
187
|
|
|
188
|
+
**Отказ от закона снимает его правила и паттерны сам.** Связь читается из вступления ресурсов —
|
|
189
|
+
у правила стоит `law`, у паттерна `rule`, — и перечислять потомков в `skip` не надо: строка,
|
|
190
|
+
которая после этого ничего не снимает, называется вслух предупреждением, а раскладка идёт
|
|
191
|
+
дальше. Каскад идёт только сверху вниз: отказ от паттерна ни правила, ни закона не трогает.
|
|
192
|
+
|
|
193
|
+
Правило, названное в `only` при невыбранном законе, тоже называется вслух: раскладка говорит,
|
|
194
|
+
что оно не приедет и по какому родителю. Файл такого правила, разложенный прежней настройкой,
|
|
195
|
+
перечисляется отдельно от брошенного — дерево от него не отказывалось.
|
|
196
|
+
|
|
197
|
+
Ресурс, ушедший из набора совсем, пакет помнит по имени: файл, положенный им в дерево прежней
|
|
198
|
+
редакцией, назван при раскладке вместе с причиной снятия. Убирает его дерево — в чужие файлы
|
|
199
|
+
пакет не пишет.
|
|
200
|
+
|
|
188
201
|
## Конфиг
|
|
189
202
|
|
|
190
203
|
`.claude/rt-kit.json` коммитится: раскладка обязана повторяться на чужой машине без вопросов.
|
|
@@ -212,7 +225,8 @@ npx agent-kit init --laws access,delivery,verifiability
|
|
|
212
225
|
называет: перечислив законы, проект говорит о законах, а не обо всём, что пакет везёт, —
|
|
213
226
|
шаблоны при нём остаются.
|
|
214
227
|
- **`skip`** — ресурсы, от которых проект отказался, теми же идентификаторами. Вычитает из
|
|
215
|
-
выбранного, поэтому отказ от одного закона не требует переписывать весь список.
|
|
228
|
+
выбранного, поэтому отказ от одного закона не требует переписывать весь список. Правила при
|
|
229
|
+
отвергнутом законе и паттерны при них перечислять не надо: их снимает каскад.
|
|
216
230
|
|
|
217
231
|
## Надстройки
|
|
218
232
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Проверка того, что набор гейта пуша не уже набора конвейера.
|
|
4
|
+
*
|
|
5
|
+
* Гейт пуша — обещание, что пуш не приедет красным. Пока его набор уже набора
|
|
6
|
+
* конвейера, обещание шире того, что гейт проверяет, и неверно оно молча: ни
|
|
7
|
+
* гейт, ни его вывод не говорят, чего в нём нет. Дважды подряд правка, прошедшая
|
|
8
|
+
* гейт целиком, была отбита конвейером — и оба раза зелёный гейт был прочитан
|
|
9
|
+
* как «локально всё зелено».
|
|
10
|
+
*
|
|
11
|
+
* Файл конвейера машиной не толкуется: команды там произвольны, а часть шагов
|
|
12
|
+
* локально неисполнима вовсе — кэш, выгрузка следов, вход в реестр. Отсюда
|
|
13
|
+
* берутся только ИМЕНА шагов, а чем каждое из них закрывается в гейте,
|
|
14
|
+
* объявляет дерево. Необъявленное имя краснеет: дыра видна на месте, а не
|
|
15
|
+
* выводится сверкой двух списков глазами.
|
|
16
|
+
*
|
|
17
|
+
* Объявление бывает двух родов. Строка — та самая команда, которую печатает
|
|
18
|
+
* `rt_push_checks` профиля; она сверяется с его выводом, потому что объявление
|
|
19
|
+
* без исполнения — та же дыра. Исключение — причина, по которой шага в гейте
|
|
20
|
+
* нет; пустая причина исключением не считается: она единственное, чем
|
|
21
|
+
* постоянная дыра отличается от забытой строки.
|
|
22
|
+
*
|
|
23
|
+
* Дерево без файла конвейера сверки не получает: проверка, падающая там, где
|
|
24
|
+
* конвейера нет, отбивала бы работу вместо промаха.
|
|
25
|
+
*
|
|
26
|
+
* Ненулевой код возврата и перечень расхождений.
|
|
27
|
+
*/
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
33
|
+
|
|
34
|
+
const GATE = CONFIG.pushGate ?? {};
|
|
35
|
+
const PIPELINE = GATE.pipelineFile ?? '';
|
|
36
|
+
const DECLARED = GATE.steps ?? {};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Профиль дерева ищется той же цепочкой, что и у гарда пуша: умолчание пакета, поверх него
|
|
40
|
+
* надстройка проекта. Разойдись они — проверка судила бы не тот набор, который гоняет гард.
|
|
41
|
+
*/
|
|
42
|
+
const PROFILES = ['.claude/rt-kit/defaults/project.sh', '.claude/rt-kit/project.sh'];
|
|
43
|
+
|
|
44
|
+
if (!PIPELINE || !existsSync(join(ROOT, PIPELINE))) {
|
|
45
|
+
console.log('check-push-gate: пропущено — файла конвейера в дереве нет');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Имена шагов конвейера. Образец задаёт дерево: у каждого хостинга своя форма записи, и
|
|
51
|
+
* угадывать её за все три пакет не берётся. Умолчание — форма GitHub Actions.
|
|
52
|
+
*/
|
|
53
|
+
const stepPattern = new RegExp(GATE.stepPattern ?? '^\\s*-\\s*name:\\s*(.+?)\\s*$');
|
|
54
|
+
|
|
55
|
+
function pipelineSteps() {
|
|
56
|
+
return readFileSync(join(ROOT, PIPELINE), 'utf8')
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((line) => line.match(stepPattern)?.[1])
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((name) => name.replace(/^['"]|['"]$/g, ''));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Набор гейта берётся у профиля его же оболочкой, а не переписывается сюда: два списка одного
|
|
65
|
+
* набора расходятся молча, и расходиться начинают в тот день, когда правят один из них.
|
|
66
|
+
*
|
|
67
|
+
* Профиля нет, функции в нём нет или оболочка отказала — набор неизвестен, и тогда проверка
|
|
68
|
+
* судит только полноту объявлений: сверять строки не с чем.
|
|
69
|
+
*/
|
|
70
|
+
function gateChecks() {
|
|
71
|
+
const present = PROFILES.filter((path) => existsSync(join(ROOT, path)));
|
|
72
|
+
if (present.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const script = `${present.map((path) => `. "${path}"`).join('\n')}
|
|
76
|
+
command -v rt_push_checks >/dev/null 2>&1 || exit 42
|
|
77
|
+
rt_push_checks ""`;
|
|
78
|
+
try {
|
|
79
|
+
return execFileSync('bash', ['-c', script], { cwd: ROOT, encoding: 'utf8' })
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((line) => line.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const steps = pipelineSteps();
|
|
89
|
+
const checks = gateChecks();
|
|
90
|
+
const problems = [];
|
|
91
|
+
|
|
92
|
+
for (const step of steps) {
|
|
93
|
+
const declaration = DECLARED[step];
|
|
94
|
+
|
|
95
|
+
if (declaration === undefined) {
|
|
96
|
+
problems.push(
|
|
97
|
+
`шаг конвейера «${step}» не объявлен: закрой его строкой набора в pushGate.steps ` + 'либо объяви исключением с причиной'
|
|
98
|
+
);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof declaration === 'object' && declaration !== null) {
|
|
103
|
+
const reason = String(declaration.skip ?? '').trim();
|
|
104
|
+
if (!reason) {
|
|
105
|
+
problems.push(`шаг конвейера «${step}» объявлен исключением без причины — пустая причина не считается`);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const line = String(declaration).trim();
|
|
111
|
+
if (!line) {
|
|
112
|
+
problems.push(`шаг конвейера «${step}» объявлен пустой строкой — назови команду либо объяви исключение`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (checks && !checks.some((check) => check.includes(line))) {
|
|
117
|
+
problems.push(`шаг конвейера «${step}» объявлен строкой «${line}», а набор гейта её не печатает`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Строка о шаге, которого в конвейере нет, — устаревшая: иначе объявления копят мёртвое. */
|
|
122
|
+
const known = new Set(steps);
|
|
123
|
+
for (const step of Object.keys(DECLARED)) {
|
|
124
|
+
if (!known.has(step)) {
|
|
125
|
+
problems.push(`объявление «${step}» устарело — такого шага в ${PIPELINE} нет`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (problems.length > 0) {
|
|
130
|
+
console.error(`check-push-gate: расхождений ${problems.length}\n`);
|
|
131
|
+
problems.forEach((problem) => console.error(` ${problem}`));
|
|
132
|
+
console.error('\nНабор гейта пуша не бывает уже набора конвейера — правило поставки.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const skipped = Object.values(DECLARED).filter((value) => typeof value === 'object' && value !== null).length;
|
|
137
|
+
console.log(
|
|
138
|
+
`check-push-gate: шагов конвейера ${steps.length}, закрыто набором ${steps.length - skipped}, ` + `объявлено исключениями ${skipped}`
|
|
139
|
+
);
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* видно ни одному из них. Эта проверка отвечает на другой вопрос — «а сколько такого в
|
|
9
9
|
* дереве сейчас», — и потому смотрит на файл целиком, а не на правку.
|
|
10
10
|
*
|
|
11
|
-
* Признаки
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Признаки не лежат здесь: они объявлены наборами по пакетам rt-tools, и дерево называет в
|
|
12
|
+
* настройке проверок те, что берёт. Тот же список читает гард — расходиться им нельзя, иначе
|
|
13
|
+
* правка проходит гард и падает на гейте. Свои признаки дерево дописывает своим файлом.
|
|
14
|
+
*
|
|
15
|
+
* Отличий от гарда два. Первое: инвентарь кита не читается — гард спрашивает диск, потому что
|
|
16
|
+
* отвечает одной правке, а сплошной проверке важно накопленное, и пропавший пакет молча
|
|
17
|
+
* обнулял бы сводку. Второе: маркер `native-ok` снимает свою строку, а не весь файл.
|
|
16
18
|
*
|
|
17
19
|
* Накопленное к моменту заведения проверки лежит в tools/reuse-allowlist.json и отказом не
|
|
18
20
|
* считается: гейт падает на новом расхождении, а старое остаётся видимым числом в сводке.
|
|
@@ -24,109 +26,40 @@ import { readFileSync, readdirSync } from 'node:fs';
|
|
|
24
26
|
import { join } from 'node:path';
|
|
25
27
|
|
|
26
28
|
import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
29
|
+
import { loadSignals } from './signals.mjs';
|
|
27
30
|
|
|
28
31
|
const ALLOWLIST = allowlistOf('reuse');
|
|
29
32
|
const SOURCE_ROOTS = CONFIG.sourceRoots;
|
|
30
33
|
const SKIPPED_DIRS = CONFIG.skippedDirs;
|
|
31
34
|
const BACKEND_ROOTS = CONFIG.backendRoots;
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
* Признак расхождения: чем он виден в тексте и на что готовое его меняет. Порядок строк —
|
|
35
|
-
* порядок в сводке, поэтому родственные признаки стоят рядом.
|
|
36
|
-
*/
|
|
37
|
-
const SIGNALS = [
|
|
38
|
-
{
|
|
39
|
-
key: 'input',
|
|
40
|
-
ext: '.html',
|
|
41
|
-
find: (text) => count(text, /<input\b/g),
|
|
42
|
-
instead: 'rt-input / rt-input-number / rt-checkbox / rt-file-input',
|
|
43
|
-
},
|
|
44
|
-
{ key: 'textarea', ext: '.html', find: (text) => count(text, /<textarea\b/g), instead: 'rt-textarea' },
|
|
45
|
-
{ key: 'select', ext: '.html', find: (text) => count(text, /<select\b/g), instead: 'rt-select / rt-multiselect' },
|
|
46
|
-
{
|
|
47
|
-
key: 'button',
|
|
48
|
-
ext: '.html',
|
|
49
|
-
find: (text) => count(withoutKitButtons(text), /<button\b/g),
|
|
50
|
-
instead: 'button[rtButton] / rt-icon-button',
|
|
51
|
-
},
|
|
52
|
-
{ key: 'table', ext: '.html', find: (text) => count(text, /<table\b/g), instead: 'rt-table' },
|
|
53
|
-
{ key: 'dialog', ext: '.html', find: (text) => count(text, /<dialog\b/g), instead: 'rt-dialog' },
|
|
54
|
-
{ key: 'alert', ext: '.html', find: (text) => count(text, /role="alert"/g), instead: 'rt-message' },
|
|
55
|
-
|
|
56
|
-
{ key: 'overlay', ext: '.scss', find: fullScreenOverlays, instead: 'rt-dialog / rt-aside / rt-bottom-sheet' },
|
|
57
|
-
{
|
|
58
|
-
key: 'backdrop',
|
|
59
|
-
ext: '.scss',
|
|
60
|
-
find: (text) => count(text, /backdrop-filter|background(-color)?: *(rgba\( *0 *, *0 *, *0|rgb\( *0 +0 +0)/g),
|
|
61
|
-
instead: 'backdrop рисует rt-dialog',
|
|
62
|
-
},
|
|
63
|
-
{ key: 'z-index', ext: '.scss', find: (text) => count(text, /z-index: *\d{4,}/g), instead: 'слой оверлеев кита уже выше' },
|
|
64
|
-
{ key: 'spin', ext: '.scss', find: (text) => count(text, /@keyframes[^{]*(spin|rotate|loading)/g), instead: 'rt-spinner' },
|
|
65
|
-
{
|
|
66
|
-
key: 'shimmer',
|
|
67
|
-
ext: '.scss',
|
|
68
|
-
find: (text) => count(text, /@keyframes[^{]*(shimmer|skeleton|pulse)/g),
|
|
69
|
-
instead: 'rt-skeleton / rt-skeleton-wrapper',
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
{ key: 'inline-template', ext: '.component.ts', find: (text) => count(text, /^ *template: *['"`]/gm), instead: 'шаблон в своём .html' },
|
|
73
|
-
// Список стилей открывается своей строкой, а строка с кавычкой идёт следующей
|
|
74
|
-
{
|
|
75
|
-
key: 'inline-styles',
|
|
76
|
-
ext: '.component.ts',
|
|
77
|
-
find: (text) => count(text, /^ *styles: *(\[\s*)?['"`\n]/gm),
|
|
78
|
-
instead: 'стили в своём .scss',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
key: 'value-accessor',
|
|
82
|
-
ext: '.component.ts',
|
|
83
|
-
find: (text) => (/ControlValueAccessor|NG_VALUE_ACCESSOR/.test(text) && !/extends VmFormControlBase/.test(text) ? 1 : 0),
|
|
84
|
-
instead: 'VmFormControlBase',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
key: 'mapper-base',
|
|
88
|
-
ext: '.mapper.ts',
|
|
89
|
-
// На бэкенде перевод сущности написан свободными функциями — это долг `Q-S-1`,
|
|
90
|
-
// а не место для этой проверки. Где лежит бэкенд, знает настройка дерева: зашитый здесь
|
|
91
|
-
// корень молча проверял бы фронтовым мерилом чужой код у всякого, кто держит его иначе.
|
|
92
|
-
skip: (path) => BACKEND_ROOTS.some((root) => path.startsWith(root)),
|
|
93
|
-
find: (text) => (/class +[A-Za-z0-9_]+Mapper/.test(text) && !/extends BaseMapper/.test(text) ? 1 : 0),
|
|
94
|
-
instead: 'BaseMapper и this.typeCast',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
key: 'procedure-mark',
|
|
98
|
-
ext: '.procedure.ts',
|
|
99
|
-
find: (text) => (/export class/.test(text) && !/@ConnectProcedure/.test(text) ? 1 : 0),
|
|
100
|
-
instead: '@ConnectProcedure()',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
key: 'aside-base',
|
|
104
|
-
ext: '.component.ts',
|
|
105
|
-
onlyNamed: /aside[^/]*\.component\.ts$/,
|
|
106
|
-
find: (text) => (/export class/.test(text) && !/extends VmRouteAsideComponent/.test(text) ? 1 : 0),
|
|
107
|
-
instead: 'VmRouteAsideComponent',
|
|
108
|
-
},
|
|
109
|
-
];
|
|
36
|
+
const SIGNALS = loadSignals(CONFIG.reuse ?? {}, ROOT);
|
|
110
37
|
|
|
111
38
|
function count(text, expression) {
|
|
112
39
|
return [...text.matchAll(expression)].length;
|
|
113
40
|
}
|
|
114
41
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Сколько раз признак виден в тексте.
|
|
44
|
+
*
|
|
45
|
+
* `strip` вычёркивает предписанный вариант до счёта: у кнопки кита та же подстрока `<button`, и
|
|
46
|
+
* без вычёркивания она считалась бы нарушением сама по себе. `all` требует совпадения всех
|
|
47
|
+
* образцов разом — так описан хост, растянутый на весь экран. `cancel` гасит признак целиком:
|
|
48
|
+
* основа уже унаследована, готовое уже позвано.
|
|
49
|
+
*/
|
|
50
|
+
function found(signal, text) {
|
|
51
|
+
const body = signal.strip ? text.replace(new RegExp(signal.strip, 'gs'), '') : text;
|
|
52
|
+
if (signal.cancel && new RegExp(signal.cancel).test(body)) {
|
|
123
53
|
return 0;
|
|
124
54
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
55
|
+
if (signal.all?.some((one) => !new RegExp(one).test(body))) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (signal.mode === 'presence') {
|
|
59
|
+
return new RegExp(signal.find).test(body) ? 1 : 0;
|
|
60
|
+
}
|
|
128
61
|
|
|
129
|
-
return
|
|
62
|
+
return count(body, new RegExp(signal.find, signal.flags ?? 'g'));
|
|
130
63
|
}
|
|
131
64
|
|
|
132
65
|
function collectFiles(dir) {
|
|
@@ -171,12 +104,13 @@ for (const root of SOURCE_ROOTS) {
|
|
|
171
104
|
for (const path of collectFiles(root).filter(judged).sort()) {
|
|
172
105
|
const text = withoutMarked(readFileSync(join(ROOT, path), 'utf8'));
|
|
173
106
|
for (const signal of SIGNALS) {
|
|
174
|
-
|
|
107
|
+
const skipped = signal.skipBackendRoots && BACKEND_ROOTS.some((root) => path.startsWith(root));
|
|
108
|
+
if (!path.endsWith(signal.ext) || skipped || (signal.onlyNamed && !new RegExp(signal.onlyNamed).test(path))) {
|
|
175
109
|
continue;
|
|
176
110
|
}
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
findings.push({ key: `${signal.key} ×${
|
|
111
|
+
const times = found(signal, text);
|
|
112
|
+
if (times > 0) {
|
|
113
|
+
findings.push({ key: `${signal.key} ×${times} @ ${path}`, instead: signal.instead });
|
|
180
114
|
}
|
|
181
115
|
}
|
|
182
116
|
}
|
|
@@ -48,6 +48,14 @@ const DEFAULTS = {
|
|
|
48
48
|
* вносится только исходник. Пусто — переносимых текстов дерево не держит.
|
|
49
49
|
*/
|
|
50
50
|
portableDirs: [],
|
|
51
|
+
/**
|
|
52
|
+
* Признаки единообразия: какие наборы дерево берёт и где лежат его собственные.
|
|
53
|
+
*
|
|
54
|
+
* Наборы режутся по пакетам, чьё готовое они называют, и дерево объявляет только те, что
|
|
55
|
+
* ставит: признак о готовом из пакета, которого в дереве нет, отвечает ложно. Пусто —
|
|
56
|
+
* признаков нет вовсе, и гард с проверкой об этом говорят, а не молчат.
|
|
57
|
+
*/
|
|
58
|
+
reuse: { bundles: [], signals: '' },
|
|
51
59
|
/** Где лежат спеки доменов; пусто — их в дереве нет, и сверка спеков не запускается. */
|
|
52
60
|
specsDir: 'docs/specs',
|
|
53
61
|
tasksDir: 'docs/tasks',
|
|
@@ -84,6 +92,17 @@ const DEFAULTS = {
|
|
|
84
92
|
* не считается никогда.
|
|
85
93
|
*/
|
|
86
94
|
productionMarks: [],
|
|
95
|
+
/**
|
|
96
|
+
* Гейт пуша против конвейера. `pipelineFile` — файл, из которого берутся имена шагов;
|
|
97
|
+
* пусто или файла нет — сверки нет вовсе. `stepPattern` — чем имя шага оттуда достаётся;
|
|
98
|
+
* умолчание записано под GitHub Actions. `steps` — чем каждое имя закрыто: строка набора
|
|
99
|
+
* либо `{ "skip": "<причина>" }`. Необъявленный шаг отбивает пуш.
|
|
100
|
+
*/
|
|
101
|
+
pushGate: {
|
|
102
|
+
pipelineFile: '',
|
|
103
|
+
stepPattern: '^\\s*-\\s*name:\\s*(.+?)\\s*$',
|
|
104
|
+
steps: {},
|
|
105
|
+
},
|
|
87
106
|
/** Очередь работ: владелец, репозиторий, борда и учётная запись машинной работы. */
|
|
88
107
|
board: {
|
|
89
108
|
owner: '',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Признаки самого фреймворка: ничьих имён не называют и верны любому дереву на Angular.",
|
|
3
|
+
"signals": [
|
|
4
|
+
{ "key": "inline-template", "ext": ".component.ts", "find": "^ *template: *['\"`]", "flags": "gm", "instead": "шаблон в своём .html" },
|
|
5
|
+
{
|
|
6
|
+
"key": "inline-styles",
|
|
7
|
+
"ext": ".component.ts",
|
|
8
|
+
"find": "^ *styles: *(\\[\\s*)?['\"`\\n]",
|
|
9
|
+
"flags": "gm",
|
|
10
|
+
"instead": "стили в своём .scss"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"key": "decorator-io",
|
|
14
|
+
"ext": ".ts",
|
|
15
|
+
"find": "@(Input|Output|ViewChild|ViewChildren|ContentChild)\\(",
|
|
16
|
+
"instead": "input(), output(), viewChild(), contentChild()"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Признаки пакета @rt-tools/core: директивы BEM, доступ к среде и хранилищу браузера.",
|
|
3
|
+
"signals": [
|
|
4
|
+
{ "key": "class-binding", "ext": ".html", "find": "\\[ngClass\\]|\\[class\\.", "instead": "директивы rtBlock, rtElem, [rtMod]" },
|
|
5
|
+
{
|
|
6
|
+
"key": "raw-storage",
|
|
7
|
+
"ext": ".ts",
|
|
8
|
+
"find": "(window\\.)?(local|session)Storage\\.",
|
|
9
|
+
"instead": "службы хранилища @rt-tools/core"
|
|
10
|
+
},
|
|
11
|
+
{ "key": "raw-media-query", "ext": ".ts", "find": "matchMedia\\(", "instead": "BreakpointsService" },
|
|
12
|
+
{
|
|
13
|
+
"key": "raw-window",
|
|
14
|
+
"ext": ".ts",
|
|
15
|
+
"find": "(^|[^.\\w])(window|document)\\.",
|
|
16
|
+
"instead": "PlatformService и токены среды @rt-tools/core"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Признаки пакета @rt-tools/store: своё хранилище состояния вместо базового.",
|
|
3
|
+
"signals": [
|
|
4
|
+
{
|
|
5
|
+
"key": "store-base",
|
|
6
|
+
"ext": ".service.ts",
|
|
7
|
+
"mode": "presence",
|
|
8
|
+
"scope": "whole",
|
|
9
|
+
"find": "new BehaviorSubject|private +readonly +state *=",
|
|
10
|
+
"cancel": "extends +Base(Async)?StoreService",
|
|
11
|
+
"instead": "BaseStoreService / BaseAsyncStoreService"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Признаки пакета @rt-tools/ui-kit-v2: нативный контрол там, где кит везёт свой.",
|
|
3
|
+
"signals": [
|
|
4
|
+
{ "key": "input", "ext": ".html", "find": "<input\\b", "instead": "rt-input / rt-input-number / rt-file-input / rt-checkbox" },
|
|
5
|
+
{ "key": "textarea", "ext": ".html", "find": "<textarea\\b", "instead": "rt-textarea" },
|
|
6
|
+
{ "key": "select", "ext": ".html", "find": "<select\\b", "instead": "rt-select / rt-multiselect" },
|
|
7
|
+
{
|
|
8
|
+
"key": "button",
|
|
9
|
+
"ext": ".html",
|
|
10
|
+
"strip": "<button\\b[^>]*rtButton[^>]*>",
|
|
11
|
+
"find": "<button\\b",
|
|
12
|
+
"instead": "button[rtButton] / rt-icon-button / rt-split-button"
|
|
13
|
+
},
|
|
14
|
+
{ "key": "table", "ext": ".html", "find": "<table\\b", "instead": "rt-table" },
|
|
15
|
+
{ "key": "dialog", "ext": ".html", "find": "<dialog\\b", "instead": "rt-dialog" },
|
|
16
|
+
{ "key": "alert", "ext": ".html", "find": "role=\"alert\"", "instead": "rt-message / rt-toast" },
|
|
17
|
+
{
|
|
18
|
+
"key": "overlay-inset",
|
|
19
|
+
"ext": ".scss",
|
|
20
|
+
"mode": "presence",
|
|
21
|
+
"find": "position: *fixed",
|
|
22
|
+
"all": ["inset: *0"],
|
|
23
|
+
"instead": "rt-dialog / rt-aside / rt-bottom-sheet"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"key": "overlay-sides",
|
|
27
|
+
"ext": ".scss",
|
|
28
|
+
"mode": "presence",
|
|
29
|
+
"find": "position: *fixed",
|
|
30
|
+
"all": ["(^|[^-])top: *0", "left: *0", "right: *0", "bottom: *0"],
|
|
31
|
+
"instead": "rt-dialog / rt-aside / rt-bottom-sheet"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"key": "backdrop",
|
|
35
|
+
"ext": ".scss",
|
|
36
|
+
"find": "backdrop-filter|background(-color)?: *(rgba\\( *0 *, *0 *, *0|rgb\\( *0 +0 +0)",
|
|
37
|
+
"instead": "подложку рисует rt-dialog"
|
|
38
|
+
},
|
|
39
|
+
{ "key": "z-index", "ext": ".scss", "find": "z-index: *\\d{4,}", "instead": "слой перекрытий кита уже выше" },
|
|
40
|
+
{ "key": "spin", "ext": ".scss", "find": "@keyframes[^{]*(spin|rotate|loading)", "instead": "rt-spinner" },
|
|
41
|
+
{
|
|
42
|
+
"key": "shimmer",
|
|
43
|
+
"ext": ".scss",
|
|
44
|
+
"find": "@keyframes[^{]*(shimmer|skeleton|pulse)",
|
|
45
|
+
"instead": "rt-skeleton / rt-skeleton-wrapper"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Признаки пакета @rt-tools/ui-kit: нативный контрол там, где кит везёт свой.",
|
|
3
|
+
"signals": [
|
|
4
|
+
{ "key": "input", "ext": ".html", "find": "<input\\b", "instead": "rtui-dynamic-input / rtui-checkbox / rtui-toggle" },
|
|
5
|
+
{ "key": "button", "ext": ".html", "find": "<button\\b", "instead": "rtui-button / rtui-multi-button" },
|
|
6
|
+
{ "key": "table", "ext": ".html", "find": "<table\\b", "instead": "rtui-table / rtui-table-container" },
|
|
7
|
+
{ "key": "dialog", "ext": ".html", "find": "<dialog\\b", "instead": "rtui-modal / rtui-aside-panel" },
|
|
8
|
+
{ "key": "alert", "ext": ".html", "find": "role=\"alert\"", "instead": "rtui-snack-bar / rtui-info-badge" },
|
|
9
|
+
{ "key": "spin", "ext": ".scss", "find": "@keyframes[^{]*(spin|rotate|loading)", "instead": "rtui-spinner" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Признаки пакета @rt-tools/utils: основа перевода сущности и приведение типов.",
|
|
3
|
+
"signals": [
|
|
4
|
+
{
|
|
5
|
+
"key": "mapper-base",
|
|
6
|
+
"ext": ".mapper.ts",
|
|
7
|
+
"mode": "presence",
|
|
8
|
+
"scope": "whole",
|
|
9
|
+
"skipBackendRoots": true,
|
|
10
|
+
"find": "class +[A-Za-z0-9_]+Mapper",
|
|
11
|
+
"cancel": "extends +BaseMapper",
|
|
12
|
+
"instead": "BaseMapper и this.typeCast"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Наборы признаков единообразия: что дерево объявило своим и что при этом читается.
|
|
3
|
+
*
|
|
4
|
+
* Признак не лежит в коде проверки. Пакет режет признаки по своим пакетам — у кита свои, у
|
|
5
|
+
* хранилища свои, — а дерево называет в настройке проверок те, что берёт. Набор, который дерево
|
|
6
|
+
* не назвало, не читается вовсе: признак о готовом из пакета, которого в дереве нет, отвечает
|
|
7
|
+
* ложно ровно так же, как имя чужого приложения.
|
|
8
|
+
*
|
|
9
|
+
* Тот же список читает гард на правке. Общий файл — единственное, чем гард на оболочке и
|
|
10
|
+
* проверка на JS могут быть связаны: расходиться им нельзя, иначе правка проходит гард и падает
|
|
11
|
+
* на гейте.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
/** Наборы лежат рядом с этим файлом: он и сам ресурс пакета, и раскладывается вместе с ними. */
|
|
18
|
+
export const BUNDLES_DIR = join(dirname(fileURLToPath(import.meta.url)), 'signals');
|
|
19
|
+
|
|
20
|
+
export function bundleNames() {
|
|
21
|
+
if (!existsSync(BUNDLES_DIR)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return readdirSync(BUNDLES_DIR)
|
|
26
|
+
.filter((name) => name.endsWith('.json'))
|
|
27
|
+
.map((name) => name.slice(0, -'.json'.length))
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Разбор набора: шапка раскладки снимается до JSON.
|
|
33
|
+
*
|
|
34
|
+
* Раскладка ставит её первой строкой и комментирует незнакомое расширение решёткой — в JSON
|
|
35
|
+
* комментария нет, и без снятия разбор падает на первом же символе.
|
|
36
|
+
*/
|
|
37
|
+
function parseSignals(text) {
|
|
38
|
+
return JSON.parse(text.replace(/^#[^\n]*\n/, '')).signals ?? [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readBundle(name) {
|
|
42
|
+
const path = join(BUNDLES_DIR, `${name}.json`);
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
const known = bundleNames().join(', ') || 'ни одного';
|
|
45
|
+
throw new Error(`набор признаков «${name}» при пакете не найден; есть: ${known}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return parseSignals(readFileSync(path, 'utf8'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Признаки объявленных наборов, а поверх них — свои признаки дерева.
|
|
53
|
+
*
|
|
54
|
+
* Совпавший ключ замещает пакетный: дерево вправе сказать о своём готовом точнее, чем пакет,
|
|
55
|
+
* который его не видел. Файла своих признаков может не быть — это не отказ: пропуск дешевле
|
|
56
|
+
* остановки работы, и объявляют его раньше, чем заводят.
|
|
57
|
+
*/
|
|
58
|
+
export function loadSignals(config, root) {
|
|
59
|
+
const declared = config.bundles ?? [];
|
|
60
|
+
const signals = declared.flatMap(readBundle);
|
|
61
|
+
|
|
62
|
+
const ownPath = config.signals ? join(root, config.signals) : null;
|
|
63
|
+
const own = ownPath && existsSync(ownPath) ? parseSignals(readFileSync(ownPath, 'utf8')) : [];
|
|
64
|
+
|
|
65
|
+
const byKey = new Map(signals.map((signal) => [signal.key, signal]));
|
|
66
|
+
for (const signal of own) {
|
|
67
|
+
byKey.set(signal.key, signal);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...byKey.values()];
|
|
71
|
+
}
|
|
@@ -58,7 +58,7 @@ rt_push_checks_default() {
|
|
|
58
58
|
[ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
|
|
59
59
|
|
|
60
60
|
for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
|
|
61
|
-
check-lib-layers check-reuse check-schema-drift; do
|
|
61
|
+
check-lib-layers check-reuse check-schema-drift check-push-gate; do
|
|
62
62
|
[ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
|
|
63
63
|
done
|
|
64
64
|
|
|
@@ -220,20 +220,22 @@ rt_task_state_default() {
|
|
|
220
220
|
# Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
|
|
221
221
|
# правку, которая ничего нового не заводит.
|
|
222
222
|
rt_reinvented_in_default() {
|
|
223
|
+
# Четыре поля через табуляцию: над чем, образец, образец отмены, чем заменить. Пустое поле
|
|
224
|
+
# пишется пустым — их читают по одному, и схлопывание третьего уносило совет в отмену.
|
|
223
225
|
case "$1" in
|
|
224
226
|
*.ts)
|
|
225
|
-
printf '%s\t%s\n' '
|
|
226
|
-
printf '%s\t%s\n' 'get [a-zA-Z]+\(\)[[:space:]]*(:|\{)' 'computed(): производное значение сигналом, а не геттером'
|
|
227
|
-
;;
|
|
228
|
-
*.html)
|
|
229
|
-
printf '%s\t%s\n' '\[ngClass\]|\[class\.' 'директивы класса кита'
|
|
227
|
+
printf '%s\t%s\t%s\t%s\n' 'added' 'get [a-zA-Z]+\(\)[[:space:]]*(:|\{)' '' 'computed(): производное значение сигналом, а не геттером'
|
|
230
228
|
;;
|
|
231
229
|
*.scss)
|
|
232
|
-
printf '%s\t%s\n' '#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?\b' 'токен оформления вместо записанного цвета'
|
|
230
|
+
printf '%s\t%s\t%s\t%s\n' 'added' '#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?\b' '' 'токен оформления вместо записанного цвета'
|
|
233
231
|
;;
|
|
234
232
|
esac
|
|
235
233
|
}
|
|
236
234
|
|
|
235
|
+
# Где лежат наборы признаков единообразия — они раскладываются рядом с проверками, а раскладка
|
|
236
|
+
# проверок у каждого дерева своя. Читают их и гард на правке, и сплошная сверка.
|
|
237
|
+
RT_REUSE_SIGNALS_DIR="${RT_REUSE_SIGNALS_DIR:-tools/signals}"
|
|
238
|
+
|
|
237
239
|
# Где якорь для спек не требуется. Витрина, корневая разметка и сборка — общее у всех деревьев;
|
|
238
240
|
# своё дерево дописывает надстройкой.
|
|
239
241
|
RT_QA_SKIP_RE="${RT_QA_SKIP_RE:-/node_modules/|/dist/|\.stories\.html\$|/src/index\.html\$}"
|