@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.
@@ -8,11 +8,13 @@
8
8
  * видно ни одному из них. Эта проверка отвечает на другой вопрос — «а сколько такого в
9
9
  * дереве сейчас», — и потому смотрит на файл целиком, а не на правку.
10
10
  *
11
- * Признаки те же, что у гарда, и берутся оттуда же: расходиться им нельзя, иначе правка
12
- * проходит гард и падает на гейте. Отличий два. Первое: инвентарь кита не читается гард
13
- * спрашивает диск, потому что отвечает одной правке, а сплошной проверке важно накопленное,
14
- * и пропавший пакет молча обнулял бы сводку. Второе: маркер `native-ok` снимает свою
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
- /** `button[rtButton]` — предписанный вариант, а подстрока `<button` у него та же */
116
- function withoutKitButtons(text) {
117
- return text.replace(/<button\b[^>]*rtButton[^>]*>/gs, '');
118
- }
119
-
120
- /** Хост, растянутый на весь экран: `inset: 0` либо все четыре стороны в нуле */
121
- function fullScreenOverlays(text) {
122
- if (!/position: *fixed/.test(text)) {
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
- const stretched =
126
- /inset: *0/.test(text) ||
127
- (/(^|[^-])top: *0/.test(text) && /left: *0/.test(text) && /right: *0/.test(text) && /bottom: *0/.test(text));
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 stretched ? 1 : 0;
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
- if (!path.endsWith(signal.ext) || signal.skip?.(path) || (signal.onlyNamed && !signal.onlyNamed.test(path))) {
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 found = signal.find(text);
178
- if (found > 0) {
179
- findings.push({ key: `${signal.key} ×${found} @ ${path}`, instead: signal.instead });
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' '@(Input|Output|ViewChild|ViewChildren|ContentChild)\(' 'input(), output(), viewChild(), contentChild() реактивный вход и выход'
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
- while IFS="$(printf '\t')" read -r scope pattern cancel replacement; do
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
- - в шаблоне фичи стоит нативный `<input>`, `<button>`, `<select>`, `<table>` или `<dialog>`;
71
- - отказ или предупреждение собраны руками свой `role="alert"` вместо `rt-message`;
72
- - в `.scss` фичи появились `position: fixed` на весь экран, свой backdrop, `z-index` от тысячи,
73
- `@keyframes` вращения или мерцания;
74
- - в файле стилей экрана объявлена раскладка`display: flex` с `gap` и `padding` на `:host`;
75
- - компонент реализует `ControlValueAccessor` сам, а не наследует `RtFormControlBase`;
76
- - имя файла кончается на `-button`, `-input`, `-dialog`, `-spinner` или `-table` вне
77
- кита;
78
- - маппер переводит поля вручную, без `BaseMapper`;
79
- - процедура бэкенда объявлена без `@ConnectProcedure()`.
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
- | раздел разложенного текста | `.claude/rt-kit/overrides/<идентификатор ресурса>` |
28
- | что брать, а от чего отказаться | `.claude/rt-kit.json`, ключи `only` и `skip` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/agent-kit",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Переносимый слой правил для агента: законы, хуки, проверки и агенты, раскладываемые в репозиторий одной командой",
5
5
  "author": "RT Team",
6
6
  "license": "Apache-2.0",
Binary file
Binary file