@rt-tools/agent-kit 0.7.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/assets/checks/check-reuse.mjs +32 -98
- package/assets/checks/rt-kit-checks.config.mjs +8 -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 +8 -6
- package/assets/hooks/reuse-first-guard.sh +90 -1
- package/assets/patterns/reuse-first-extend.md +30 -0
- package/assets/rules/reuse-first.md +31 -13
- package/assets/skills/agent-kit.md +13 -8
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.8.0.tgz +0 -0
- package/rt-tools-agent-kit-0.7.0.tgz +0 -0
|
@@ -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',
|
|
@@ -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
|
+
}
|
|
@@ -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\$}"
|
|
@@ -132,8 +132,90 @@ has_re() {
|
|
|
132
132
|
printf '%s' "$1" | RT_RE="$2" perl -0777 -ne 'exit(/$ENV{RT_RE}/s ? 0 : 1)' 2>/dev/null
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
# Что дерево объявило своим: наборы признаков и файл собственных. Настройка проверок — та же,
|
|
136
|
+
# что читает сплошная проверка; каталог наборов называет профиль дерева, потому что раскладка
|
|
137
|
+
# проверок у каждого дерева своя.
|
|
138
|
+
rt_checks_json="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/checks.json"
|
|
139
|
+
rt_bundles=''
|
|
140
|
+
rt_own_signals=''
|
|
141
|
+
if [ -f "$rt_checks_json" ]; then
|
|
142
|
+
rt_bundles="$(jq -r '.reuse.bundles[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
|
|
143
|
+
own="$(jq -r '.reuse.signals // empty' "$rt_checks_json" 2>/dev/null)"
|
|
144
|
+
[ -n "$own" ] && rt_own_signals="${CLAUDE_PROJECT_DIR:-.}/$own"
|
|
145
|
+
fi
|
|
146
|
+
rt_signals_dir="${CLAUDE_PROJECT_DIR:-.}/${RT_REUSE_SIGNALS_DIR:-tools/signals}"
|
|
147
|
+
|
|
148
|
+
# Признаки объявленных наборов: те же файлы читает сплошная проверка. Ключ признака совпал с
|
|
149
|
+
# ключом дерева — побеждает дерево: оно видит своё готовое, а пакет его не видел.
|
|
150
|
+
signals_json() {
|
|
151
|
+
[ -n "$rt_signals_dir" ] || return 0
|
|
152
|
+
[ -d "$rt_signals_dir" ] || return 0
|
|
153
|
+
# Шапка раскладки снимается до разбора: в JSON комментария нет, и с ней разбор падает.
|
|
154
|
+
for name in $rt_bundles; do
|
|
155
|
+
file="$rt_signals_dir/$name.json"
|
|
156
|
+
[ -f "$file" ] && grep -v '^# rt-kit ' "$file" | jq -c '.signals[]?' 2>/dev/null
|
|
157
|
+
done
|
|
158
|
+
[ -n "$rt_own_signals" ] && [ -f "$rt_own_signals" ] && grep -v '^# rt-kit ' "$rt_own_signals" | jq -c '.signals[]?' 2>/dev/null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Поля читаются по одному, а не разбором строки: таб в `IFS` — пробельный разделитель, и пустое
|
|
162
|
+
# поле в середине схлопывается, из-за чего совет уезжает в образец отмены и гасит признак молча.
|
|
163
|
+
field() { printf '%s' "$1" | jq -r "$2 // empty" 2>/dev/null; }
|
|
164
|
+
|
|
135
165
|
found=''
|
|
136
|
-
|
|
166
|
+
signals_seen=0
|
|
167
|
+
while IFS= read -r signal; do
|
|
168
|
+
[ -z "$signal" ] && continue
|
|
169
|
+
signals_seen=1
|
|
170
|
+
ext="$(field "$signal" '.ext')"
|
|
171
|
+
case "$ext" in
|
|
172
|
+
'') ;;
|
|
173
|
+
*) case "$path" in *"$ext") ;; *) continue ;; esac ;;
|
|
174
|
+
esac
|
|
175
|
+
only_named="$(field "$signal" '.onlyNamed')"
|
|
176
|
+
if [ -n "$only_named" ]; then
|
|
177
|
+
printf '%s' "${path##*/}" | grep -qE "$only_named" || continue
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
case "$(field "$signal" '.scope')" in
|
|
181
|
+
whole) text="$whole" ;;
|
|
182
|
+
*) text="$added" ;;
|
|
183
|
+
esac
|
|
184
|
+
|
|
185
|
+
pattern="$(field "$signal" '.find')"
|
|
186
|
+
[ -z "$pattern" ] && continue
|
|
187
|
+
strip="$(field "$signal" '.strip')"
|
|
188
|
+
[ -n "$strip" ] && text="$(printf '%s' "$text" | RT_RE="$strip" perl -0777 -pe 's/$ENV{RT_RE}//gs' 2>/dev/null)"
|
|
189
|
+
|
|
190
|
+
has_re "$text" "$pattern" || continue
|
|
191
|
+
|
|
192
|
+
cancel="$(field "$signal" '.cancel')"
|
|
193
|
+
[ -n "$cancel" ] && has_re "$text" "$cancel" && continue
|
|
194
|
+
|
|
195
|
+
skip_signal=''
|
|
196
|
+
while IFS= read -r one; do
|
|
197
|
+
[ -z "$one" ] && continue
|
|
198
|
+
has_re "$text" "$one" || skip_signal=1
|
|
199
|
+
done <<ALL
|
|
200
|
+
$(printf '%s' "$signal" | jq -r '.all[]? // empty' 2>/dev/null)
|
|
201
|
+
ALL
|
|
202
|
+
[ -n "$skip_signal" ] && continue
|
|
203
|
+
|
|
204
|
+
found="${found}
|
|
205
|
+
- $(field "$signal" '.instead')"
|
|
206
|
+
done <<EOF
|
|
207
|
+
$(signals_json | jq -s -c 'reduce .[] as $one ({}; .[$one.key] = $one) | .[]' 2>/dev/null)
|
|
208
|
+
EOF
|
|
209
|
+
|
|
210
|
+
# Функция профиля остаётся вторым источником: деревья её уже написали. Поля читаются построчно,
|
|
211
|
+
# по тем же четырём колонкам, что объявлены выше.
|
|
212
|
+
while IFS= read -r line; do
|
|
213
|
+
[ -z "$line" ] && continue
|
|
214
|
+
signals_seen=1
|
|
215
|
+
scope="$(printf '%s' "$line" | cut -f1)"
|
|
216
|
+
pattern="$(printf '%s' "$line" | cut -f2)"
|
|
217
|
+
cancel="$(printf '%s' "$line" | cut -f3)"
|
|
218
|
+
replacement="$(printf '%s' "$line" | cut -f4)"
|
|
137
219
|
[ -z "$pattern" ] && continue
|
|
138
220
|
case "$scope" in
|
|
139
221
|
whole) text="$whole" ;;
|
|
@@ -147,6 +229,13 @@ done <<EOF
|
|
|
147
229
|
$(rt_reinvented_in "$path" 2>/dev/null)
|
|
148
230
|
EOF
|
|
149
231
|
|
|
232
|
+
# Гард без единого признака неотличим от гарда, которому нечего отбивать. Правку он пропускает —
|
|
233
|
+
# останавливать работу за ненастроенное дерево не за что, — но говорит, чем это настраивается.
|
|
234
|
+
if [ "$signals_seen" = 0 ]; then
|
|
235
|
+
printf '%s\n' 'reuse-first-guard: признаков нет — объявите наборы ключом `reuse.bundles` в настройке проверок' >&2
|
|
236
|
+
exit 0
|
|
237
|
+
fi
|
|
238
|
+
|
|
150
239
|
[ -z "$found" ] && exit 0
|
|
151
240
|
|
|
152
241
|
reason="BLOCKED: это уже написано. Файл: ${path##*/}
|
|
@@ -45,6 +45,36 @@ grep -rn "<похожий приём>" libs/admin libs/site --include='*.html' |
|
|
|
45
45
|
Спрашивается до того, как написан первый файл. То же относится к своей базе и к своему
|
|
46
46
|
инлайновому стилю.
|
|
47
47
|
|
|
48
|
+
## Своё готовое объявляется признаком
|
|
49
|
+
|
|
50
|
+
Заведённое с одобрения владельца видит только тот, кто о нём знает. Чтобы следующий его не
|
|
51
|
+
обошёл, дерево дописывает признак в свой файл признаков — тот, что назван ключом
|
|
52
|
+
`reuse.signals` в настройке проверок:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"key": "<короткое имя признака>",
|
|
57
|
+
"ext": ".ts",
|
|
58
|
+
"mode": "presence",
|
|
59
|
+
"scope": "whole",
|
|
60
|
+
"find": "implements +<интерфейс, который закрывает основа>",
|
|
61
|
+
"cancel": "extends +<основа>",
|
|
62
|
+
"instead": "<основа> — она уже держит <что именно>"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Область объявляется явно. `whole` читает правку вместе с содержимым файла и берётся там, где
|
|
67
|
+
признак судит устройство класса: наследование и метка объявлены один раз и в точечную правку не
|
|
68
|
+
попадают, поэтому признак без `whole` молчит всегда. `added` читает только добавленный текст и
|
|
69
|
+
берётся там, где признак судит саму строку — сырой тег, свой стиль.
|
|
70
|
+
|
|
71
|
+
Отмена `cancel` обязательна везде, где готовое можно позвать законно: без неё признак отбивает и
|
|
72
|
+
того, кто основу уже унаследовал. Пустой её не оставляют — признак без отмены объявляется без
|
|
73
|
+
этого поля вовсе.
|
|
74
|
+
|
|
75
|
+
Ключ, совпавший с пакетным, замещает его: дерево вправе сказать о своём готовом точнее, чем
|
|
76
|
+
пакет, который его не видел.
|
|
77
|
+
|
|
48
78
|
## Разовое отступление объявляется маркером
|
|
49
79
|
|
|
50
80
|
```html
|
|
@@ -46,9 +46,12 @@ description: Правило под «Закон о единообразии пр
|
|
|
46
46
|
раз в общем слое приложения, а экран её только применяет теми же директивами BEM.
|
|
47
47
|
- **Готовое расширяется, а не клонируется рядом.** Недостающий вариант заводится в ките или в
|
|
48
48
|
базовом классе, и его видят остальные экраны.
|
|
49
|
-
- **Накопленное до гарда сосчитано сплошной проверкой и в список только не растёт.** Признаки
|
|
50
|
-
|
|
51
|
-
остаётся числом в сводке.
|
|
49
|
+
- **Накопленное до гарда сосчитано сплошной проверкой и в список только не растёт.** Признаки у
|
|
50
|
+
неё те же, что у гарда, потому что оба читают одни и те же объявленные наборы, а смотрит она
|
|
51
|
+
файл целиком: гейт падает на новом месте, старое остаётся числом в сводке.
|
|
52
|
+
- **Признаки объявляются деревом, а не зашиты в проверку.** Дерево берёт из rt-tools не всё, и
|
|
53
|
+
признак о готовом из пакета, которого здесь нет, отвечает ложно ровно так же, как имя чужого
|
|
54
|
+
приложения.
|
|
52
55
|
|
|
53
56
|
## Чего из закона здесь нет
|
|
54
57
|
|
|
@@ -67,16 +70,24 @@ description: Правило под «Закон о единообразии пр
|
|
|
67
70
|
|
|
68
71
|
## Признаки, по которым видно, что готовое обошли
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
73
|
+
Списком их здесь нет: признак — это данные, а не текст правила и не код проверки. Наборы лежат
|
|
74
|
+
при пакете, по файлу на пакет rt-tools, и в каждом только то, что везёт он сам. Дерево называет
|
|
75
|
+
нужные ему наборы ключом `reuse.bundles` в настройке проверок и дописывает свои признаки файлом,
|
|
76
|
+
названным ключом `reuse.signals`; совпавший ключ замещает пакетный, новый дописывается. Гард на
|
|
77
|
+
правке и сплошная проверка читают отсюда оба — разойтись им нечем.
|
|
78
|
+
|
|
79
|
+
Что бывает признаком:
|
|
80
|
+
|
|
81
|
+
- нативный контрол в шаблоне там, где кит везёт свой;
|
|
82
|
+
- отказ, предупреждение или ожидание, собранные руками вместо готового;
|
|
83
|
+
- наложение поверх страницы, объявленное своими стилями, — подложка, порядок слоёв, вращение;
|
|
84
|
+
- раскладка, объявленная в файле стилей экрана, а не применённая директивами из общего слоя;
|
|
85
|
+
- своя реализация того, что даёт основа кита, вместо наследования;
|
|
86
|
+
- имя файла, повторяющее имя китового примитива, вне кита.
|
|
87
|
+
|
|
88
|
+
Признак называет свою область: наследование основы и метка класса в точечную правку не попадают,
|
|
89
|
+
и признак, судящий их по добавленному тексту, молчит всегда. Признак называет и отмену — образец,
|
|
90
|
+
при котором он не срабатывает: основа уже унаследована, готовое уже позвано.
|
|
80
91
|
|
|
81
92
|
## Ловушки
|
|
82
93
|
|
|
@@ -90,3 +101,10 @@ description: Правило под «Закон о единообразии пр
|
|
|
90
101
|
отступ, оставаясь тем же кодом.
|
|
91
102
|
- Ответ, данный до чтения образца, образец отменяет: согласованная форма выборки списка
|
|
92
103
|
переигрывалась вместе с контрактом через два вопроса после того, как была принята.
|
|
104
|
+
- **Набор объявляется по тому, что дерево потребляет, а не по тому, что пакет везёт.** Дерево, в
|
|
105
|
+
котором кит написан, а не позван, объявив его набор, получает советы звать кит на файлах самого
|
|
106
|
+
кита: признак верен, но обращён не туда. Такое дерево объявляет только те наборы, чьё готовое
|
|
107
|
+
оно берёт со стороны.
|
|
108
|
+
- **Гард, не получивший ни одного признака, неотличим от гарда, которому нечего отбивать.** Он
|
|
109
|
+
говорит об этом сам, и первая же правка в дереве без объявленных наборов это показывает.
|
|
110
|
+
Молчание проверки признаком порядка не считается — сначала смотрят, есть ли ей чем судить.
|
|
@@ -19,14 +19,15 @@ description: Переносимый слой правил агента — за
|
|
|
19
19
|
|
|
20
20
|
## Где что настраивается
|
|
21
21
|
|
|
22
|
-
| Что меняешь | Куда правка
|
|
23
|
-
| ---------------------------------------------- |
|
|
24
|
-
| какое правило гейт требует под какой файл | `.claude/rt-kit/gate-map.sh` — своя `skill_for`
|
|
25
|
-
| порты, адреса, линтеры, форма ветки, инвентарь | `.claude/rt-kit/project.sh` — свои `rt_*`
|
|
26
|
-
| пути и идентификаторы, которыми живут проверки | `.claude/rt-kit/checks.json`
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
22
|
+
| Что меняешь | Куда правка |
|
|
23
|
+
| ---------------------------------------------- | ------------------------------------------------------ |
|
|
24
|
+
| какое правило гейт требует под какой файл | `.claude/rt-kit/gate-map.sh` — своя `skill_for` |
|
|
25
|
+
| порты, адреса, линтеры, форма ветки, инвентарь | `.claude/rt-kit/project.sh` — свои `rt_*` |
|
|
26
|
+
| пути и идентификаторы, которыми живут проверки | `.claude/rt-kit/checks.json` |
|
|
27
|
+
| какие признаки единообразия дерево берёт | `checks.json`, ключи `reuse.bundles` и `reuse.signals` |
|
|
28
|
+
| раздел разложенного текста | `.claude/rt-kit/overrides/<идентификатор ресурса>` |
|
|
29
|
+
| что брать, а от чего отказаться | `.claude/rt-kit.json`, ключи `only` и `skip` |
|
|
30
|
+
| сам механизм — гард, проверка, текст правила | ресурс в пакете |
|
|
30
31
|
|
|
31
32
|
Надстройка объявляет функцию заново и вправе позвать умолчание тем же именем с суффиксом
|
|
32
33
|
`_default`. Слияние текста идёт по разделам `## `: совпавший заголовок замещает, новый
|
|
@@ -84,6 +85,10 @@ npx agent-kit propose # отправить предложения, адр
|
|
|
84
85
|
|
|
85
86
|
## Ловушки
|
|
86
87
|
|
|
88
|
+
- **Правила и паттерны при отвергнутом законе в отказе не перечисляются.** Их снимает каскад, а
|
|
89
|
+
строка на них становится выводимой: раскладка называет её лишней вместе с законом, из-за
|
|
90
|
+
которого она перестала снимать. Отказ мерит слой законов, а не число файлов в пакете.
|
|
91
|
+
|
|
87
92
|
- **Линтер по следам правки судит файл целиком, а не внесённую правку.** Импорт, добавленный
|
|
88
93
|
отдельным шагом, отбивается как неиспользуемый ещё до того, как появится строка, которая его
|
|
89
94
|
зовёт, и работа встаёт на половине. Правка делается одним вызовом либо в порядке «сначала
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|