@rt-tools/agent-kit 0.7.0 → 0.8.1
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 +40 -104
- 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 +42 -3
- package/assets/rules/reuse-first.md +32 -14
- package/assets/skills/agent-kit-extend.md +149 -0
- package/assets/skills/agent-kit.md +21 -12
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.8.1.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)) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (signal.all?.some((one) => !new RegExp(one).test(body))) {
|
|
123
56
|
return 0;
|
|
124
57
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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) {
|
|
@@ -152,14 +85,16 @@ function judged(path) {
|
|
|
152
85
|
}
|
|
153
86
|
|
|
154
87
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
88
|
+
* Маркер — осознанное отступление, названное автором. Снимается строка, где он стоит, и та, что
|
|
89
|
+
* идёт следом: в разметке маркер ставится комментарием над кодом, потому что форматировщик
|
|
90
|
+
* разносит длинный тег по строкам и уводит первый атрибут со строки имени тега — признак считает
|
|
91
|
+
* имя тега, а маркер оказывается ниже. Дальше следующей строки маркер не достаёт: весь файл он
|
|
92
|
+
* не гасит, иначе один разрешённый случай прикрывал бы соседние.
|
|
157
93
|
*/
|
|
158
94
|
function withoutMarked(text) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.join('\n');
|
|
95
|
+
const lines = text.split('\n');
|
|
96
|
+
|
|
97
|
+
return lines.filter((line, index) => !line.includes('native-ok') && !lines[index - 1]?.includes('native-ok')).join('\n');
|
|
163
98
|
}
|
|
164
99
|
|
|
165
100
|
const allowlist = JSON.parse(readFileSync(join(ROOT, ALLOWLIST), 'utf8'));
|
|
@@ -171,12 +106,13 @@ for (const root of SOURCE_ROOTS) {
|
|
|
171
106
|
for (const path of collectFiles(root).filter(judged).sort()) {
|
|
172
107
|
const text = withoutMarked(readFileSync(join(ROOT, path), 'utf8'));
|
|
173
108
|
for (const signal of SIGNALS) {
|
|
174
|
-
|
|
109
|
+
const skipped = signal.skipBackendRoots && BACKEND_ROOTS.some((root) => path.startsWith(root));
|
|
110
|
+
if (!path.endsWith(signal.ext) || skipped || (signal.onlyNamed && !new RegExp(signal.onlyNamed).test(path))) {
|
|
175
111
|
continue;
|
|
176
112
|
}
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
findings.push({ key: `${signal.key} ×${
|
|
113
|
+
const times = found(signal, text);
|
|
114
|
+
if (times > 0) {
|
|
115
|
+
findings.push({ key: `${signal.key} ×${times} @ ${path}`, instead: signal.instead });
|
|
180
116
|
}
|
|
181
117
|
}
|
|
182
118
|
}
|
|
@@ -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
|
|
@@ -52,9 +82,18 @@ grep -rn "<похожий приём>" libs/admin libs/site --include='*.html' |
|
|
|
52
82
|
<input type="tel" qa-dataid="phone-input" />
|
|
53
83
|
```
|
|
54
84
|
|
|
55
|
-
Маркер `native-ok`
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
Маркер `native-ok` объясняет, **чего именно нет в ките**. «Эти строки были здесь раньше»
|
|
86
|
+
причиной не считается: гард вычёркивает из проверяемого текста то, что уже лежит в файле,
|
|
87
|
+
поэтому отказ означает новый текст.
|
|
88
|
+
|
|
89
|
+
Стоит он комментарием строкой выше кода или в самой строке — снимаются обе. В разметке годится
|
|
90
|
+
только первое: форматировщик разносит тег, у которого атрибуты не влезли в предел ширины, по
|
|
91
|
+
строкам, и первый атрибут всегда уезжает на строку ниже имени тега. Признак считает имя тега,
|
|
92
|
+
то есть первую строку, а маркер, поставленный атрибутом, оказывается на второй и не снимает
|
|
93
|
+
ничего. Короткий тег форматировщик не трогает — и маркер работает ровно до тех пор, пока к тегу
|
|
94
|
+
не добавили ещё один атрибут.
|
|
95
|
+
|
|
96
|
+
Дальше следующей строки маркер не достаёт: он снимает свой случай, а не блок вокруг себя.
|
|
58
97
|
|
|
59
98
|
Сверка идёт без отступов — при переезде блок меняет отступ, оставаясь тем же кодом.
|
|
60
99
|
|
|
@@ -17,7 +17,7 @@ description: Правило под «Закон о единообразии пр
|
|
|
17
17
|
| готовое | компоненты кита `@rt-tools/ui-kit-v2` с префиксом `rt-` — сегодня их больше семидесяти — и базовые классы без селектора, наследуемые в `@Component` экрана |
|
|
18
18
|
| раскладка страниц, форм и окон | `apps/<app>/src/styles/`, применяется директивами BEM |
|
|
19
19
|
| оформление части приложения | файл `.scss` рядом с компонентом |
|
|
20
|
-
| отступление, решённое владельцем | маркер `native-ok`
|
|
20
|
+
| отступление, решённое владельцем | маркер `native-ok` с объяснением — комментарием строкой выше или в самой строке |
|
|
21
21
|
|
|
22
22
|
## Где это лежит
|
|
23
23
|
|
|
@@ -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
|
+
Молчание проверки признаком порядка не считается — сначала смотрят, есть ли ей чем судить.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-kit-extend
|
|
3
|
+
description: Готовые примеры того, как дерево дописывает своё поверх пакета правил — надстройка над разложенным текстом, своя ветка карты гейта, своя функция профиля, свой признак единообразия, свой закон и правило. Брать, когда пакетный текст говорит не то, что верно здесь, гейт требует не то правило, гард судит не по тем путям, или своё поведение надо дописать, не трогая пакет. Где что настраивается и как устроена раскладка — скил agent-kit; форма нового скила — write-a-skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Как дописать своё поверх пакета
|
|
7
|
+
|
|
8
|
+
Скил `agent-kit` называет, **где** настраивается каждый род правки. Здесь — **как** выглядит
|
|
9
|
+
сама правка, на готовых примерах, и чем каждая проверяется.
|
|
10
|
+
|
|
11
|
+
Одно правило общее для всех пяти: разложенный файл не правится на месте. Он несёт шапку
|
|
12
|
+
`rt-kit v… · <ресурс> · <сумма>`, правка в нём теряется на следующей раскладке и до тех пор
|
|
13
|
+
выглядит применённой, а `sync` на такой файл отказывает вместо того, чтобы переписать молча.
|
|
14
|
+
|
|
15
|
+
## Когда брать
|
|
16
|
+
|
|
17
|
+
- Пакетный текст говорит не то, что верно здесь: имена, пути, приёмы этого дерева.
|
|
18
|
+
- Гейт требует под файл не то правило — или молчит там, где правило есть.
|
|
19
|
+
- Гард судит не по тем путям: чужие корни, свой набор проверок, своя форма ветки.
|
|
20
|
+
- Заводится своё — признак единообразия, проверка, закон с правилом, — чего пакет не везёт.
|
|
21
|
+
|
|
22
|
+
## Текст: надстройка сливается по разделам
|
|
23
|
+
|
|
24
|
+
Файл кладётся в `overrides/<идентификатор ресурса>` — путь повторяет ресурс один в один:
|
|
25
|
+
`rules/testing.md` надстраивается файлом `rules/testing.md`.
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
## Ловушки этого дерева
|
|
29
|
+
|
|
30
|
+
- **Снимок дерева правится тем же коммитом, что и объявление.** Иначе установка у соседа
|
|
31
|
+
ставит не то, что стоит здесь.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Заголовок в надстройке | Что происходит |
|
|
35
|
+
| ---------------------- | ------------------------- |
|
|
36
|
+
| есть у пакета | раздел замещается целиком |
|
|
37
|
+
| нет у пакета | дописывается в конец |
|
|
38
|
+
| есть, но тело пустое | раздел пакета снимается |
|
|
39
|
+
|
|
40
|
+
**Замещение — целиком, и это главная ловушка.** Свой пункт, дописанный под пакетным заголовком
|
|
41
|
+
`## Ловушки`, уносит все пакетные пункты этого раздела разом, и пропажу не видно ничем: файл
|
|
42
|
+
выглядит собранным. Поэтому заголовок в примере выше свой. Пакетный заголовок берут только
|
|
43
|
+
тогда, когда пакетный текст здесь неверен и его правда надо снять.
|
|
44
|
+
|
|
45
|
+
Проверяется раскладкой: `sync`, затем `sync --check` — и глазами по разложенному файлу, на
|
|
46
|
+
месте ли пакетные разделы.
|
|
47
|
+
|
|
48
|
+
От ресурса целиком отказываются не здесь, а списком `skip` в конфиге: надстройка правит текст,
|
|
49
|
+
`skip` отменяет файл.
|
|
50
|
+
|
|
51
|
+
## Гейт: своя ветка решает раньше умолчания
|
|
52
|
+
|
|
53
|
+
Гейт спрашивает `skill_for` — она печатает имя правила или молчит. Молчание значит «правила на
|
|
54
|
+
это нет», и правка проходит.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
skill_for() {
|
|
58
|
+
kind="$1"; target="$2"; written="$3"
|
|
59
|
+
|
|
60
|
+
case "$kind" in
|
|
61
|
+
edit)
|
|
62
|
+
case "$target" in
|
|
63
|
+
# Частное — всегда раньше общего: файл истории не файл компонента.
|
|
64
|
+
*.stories.ts) printf '%s\n' 'showcase'; return 0 ;;
|
|
65
|
+
esac
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
# Всё остальное разбирает умолчание пакета — иначе оно теряется целиком.
|
|
70
|
+
command -v skill_for_default >/dev/null 2>&1 && skill_for_default "$kind" "$target" "$written"
|
|
71
|
+
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Две вещи здесь обязательны. **Порядок веток:** первое совпадение выигрывает, и общая ветка,
|
|
77
|
+
поставленная выше частной, съедает её молча. **Вызов умолчания:** объявив функцию заново и не
|
|
78
|
+
позвав `_default`, дерево остаётся без всех пакетных веток сразу — а выглядит это как «гейт
|
|
79
|
+
перестал требовать правила».
|
|
80
|
+
|
|
81
|
+
Проверяется сценариями гейта из набора пакета: они гоняют карту, ничего не раскладывая.
|
|
82
|
+
|
|
83
|
+
## Гард: профиль отвечает за имена и команды
|
|
84
|
+
|
|
85
|
+
Тем же приёмом объявляются функции профиля — что гонять перед пушем, какой документ едет парой,
|
|
86
|
+
чем линтуется файл, что здесь считается кодом приложения, какая форма ветки законна.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Какой документ обязан ехать тем же коммитом. Печатает образец пути или молчит.
|
|
90
|
+
rt_docs_pair_for() {
|
|
91
|
+
case "$1" in
|
|
92
|
+
*.spec.ts) return 0 ;;
|
|
93
|
+
libs/kit/src/*/*.component.ts) printf '%s' "${1%/*}/CONTEXT\.md" ;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Печатается **образец**, а не путь: гард сверяет им состав коммита. Умолчание зовётся так же —
|
|
99
|
+
`rt_docs_pair_for_default "$@"` — везде, где своё правило случай не закрыло.
|
|
100
|
+
|
|
101
|
+
Путь приходит от корня дерева. Признак по подстроке вида `*/projects/*` совпадает и с чужим
|
|
102
|
+
каталогом за пределами репозитория — так запись в домашний каталог была отбита требованием
|
|
103
|
+
замысла, к ней не относящимся.
|
|
104
|
+
|
|
105
|
+
## Признаки и проверки: данные, а не код
|
|
106
|
+
|
|
107
|
+
Признак единообразия — данные. Дерево называет наборы пакета, чьё готовое оно берёт, и
|
|
108
|
+
дописывает свои файлом; совпавший ключ замещает пакетный.
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"sourceRoots": ["apps", "libs"],
|
|
113
|
+
"reuse": { "bundles": ["kit"], "signals": "tools/signals/own.json" }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Объект сливается ключ за ключом: назвав один ключ раздела, дерево не теряет соседних. **Список
|
|
118
|
+
— наоборот, замещается целиком**, и это ловушка: назвав `sourceRoots`, дерево получает ровно
|
|
119
|
+
названное, а не пакетные корни плюс свои. «Дописать в список» и «убрать из списка» в этой записи
|
|
120
|
+
неразличимы, поэтому список всегда пишется полностью.
|
|
121
|
+
|
|
122
|
+
Набор объявляется по тому, что дерево **потребляет**. Дерево, в котором кит написан, а не
|
|
123
|
+
позван, объявив его набор, получит советы звать кит на файлах самого кита: признак верен, но
|
|
124
|
+
обращён не туда.
|
|
125
|
+
|
|
126
|
+
## Свой закон и правило
|
|
127
|
+
|
|
128
|
+
Пакет везёт слой правил, но не запрещает свой. Закон дерева ложится рядом с пакетными, правило
|
|
129
|
+
под него — среди скилов, и связь идёт через шапку: у правила `law:` с именем закона, у паттерна
|
|
130
|
+
`rule:` с именем правила. Имя, которому ничего не отвечает, отбивает сверку связности.
|
|
131
|
+
|
|
132
|
+
Утверждение правила получает строку в спутнике `implementation.md` — привязку к файлу и символу.
|
|
133
|
+
Утверждение, которому места в коде не нашлось, в проверяемый раздел не ставится: ему место в
|
|
134
|
+
«Ловушках» прозой.
|
|
135
|
+
|
|
136
|
+
Проверяется сверкой спеков — до пуша.
|
|
137
|
+
|
|
138
|
+
## Частые промахи
|
|
139
|
+
|
|
140
|
+
- **Правка на месте вместо надстройки.** Разложенный файл узнаётся по шапке, а не по каталогу:
|
|
141
|
+
раскладка ложится в те же `tools/` и `.claude/`, где лежит своё.
|
|
142
|
+
- **Свой пункт дописан к пакетному заголовку** — пакетные пункты этого раздела ушли молча.
|
|
143
|
+
- **Функция объявлена заново без вызова `_default`** — вместе со своим случаем потеряны все
|
|
144
|
+
пакетные.
|
|
145
|
+
- **Общая ветка карты стоит выше частной** — частная не выполняется никогда.
|
|
146
|
+
- **Замена по шаблону в файле оболочки** — `case` теряет свою `esac`, и гард с ошибкой синтаксиса
|
|
147
|
+
отвечает ненулевым кодом, то есть «правка отбита». После правки — `bash -n`.
|
|
148
|
+
- **Правка ресурса пакета без сборки.** Строка запуска читает собранное, а не исходники:
|
|
149
|
+
порядок всегда один — правка, сборка, `sync`.
|
|
@@ -19,19 +19,23 @@ 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`. Слияние текста идёт по разделам `## `: совпавший заголовок замещает, новый
|
|
33
34
|
дописывается, пустой снимает раздел пакета.
|
|
34
35
|
|
|
36
|
+
Готовый пример на каждую строку этой таблицы — скил `agent-kit-extend`: как выглядит сама
|
|
37
|
+
правка, чем она проверяется и чем кончается, если положить её не туда.
|
|
38
|
+
|
|
35
39
|
## Порядок
|
|
36
40
|
|
|
37
41
|
Правка ресурса доезжает до дерева только через сборку пакета: строка запуска читает собранное,
|
|
@@ -84,6 +88,10 @@ npx agent-kit propose # отправить предложения, адр
|
|
|
84
88
|
|
|
85
89
|
## Ловушки
|
|
86
90
|
|
|
91
|
+
- **Правила и паттерны при отвергнутом законе в отказе не перечисляются.** Их снимает каскад, а
|
|
92
|
+
строка на них становится выводимой: раскладка называет её лишней вместе с законом, из-за
|
|
93
|
+
которого она перестала снимать. Отказ мерит слой законов, а не число файлов в пакете.
|
|
94
|
+
|
|
87
95
|
- **Линтер по следам правки судит файл целиком, а не внесённую правку.** Импорт, добавленный
|
|
88
96
|
отдельным шагом, отбивается как неиспользуемый ещё до того, как появится строка, которая его
|
|
89
97
|
зовёт, и работа встаёт на половине. Правка делается одним вызовом либо в порядке «сначала
|
|
@@ -103,10 +111,11 @@ npx agent-kit propose # отправить предложения, адр
|
|
|
103
111
|
написанная по путям, требует под него доменное правило — а оно уводит править файл на месте.
|
|
104
112
|
Правка на месте теряется на следующей раскладке, и до тех пор выглядит применённой. Ветка по
|
|
105
113
|
шапке ставится в карте первой и решает раньше путей.
|
|
106
|
-
- **Настройки проверок сливаются
|
|
107
|
-
|
|
108
|
-
теряет
|
|
109
|
-
|
|
114
|
+
- **Настройки проверок сливаются по ключам, а списки — замещаются.** Объект `checks.json`
|
|
115
|
+
ложится поверх умолчания ключ за ключом на любой глубине: назвав один ключ борды, дерево не
|
|
116
|
+
теряет соседних. Список приходит целиком — назвав корни исходников, дерево получает ровно
|
|
117
|
+
названное, а не умолчание вместе со своим: «дописать в список» и «убрать из списка» в этой
|
|
118
|
+
записи неразличимы.
|
|
110
119
|
- **Утверждение правила переезжает вместе с кодом.** Вынесенное в надстройку перестаёт
|
|
111
120
|
находиться по прежнему символу, и привязка в спутнике правила врёт молча — сверка спеков
|
|
112
121
|
ловит это, но только если её позвать.
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|