@rt-tools/agent-kit 0.5.3 → 0.6.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.
Files changed (51) hide show
  1. package/assets/checks/check-file-size.mjs +127 -0
  2. package/assets/checks/rt-kit-checks.config.mjs +10 -0
  3. package/assets/defaults/gate-map.sh +90 -34
  4. package/assets/defaults/project.sh +26 -3
  5. package/assets/hooks/skill-gate-layers.sh +156 -0
  6. package/assets/hooks/skill-gate.sh +11 -2
  7. package/assets/laws/code-structure.md +3 -0
  8. package/assets/laws/delivery.md +9 -0
  9. package/assets/laws/observability.md +46 -0
  10. package/assets/laws/project-documentation.md +4 -0
  11. package/assets/laws/reuse-first.md +2 -0
  12. package/assets/laws/verifiability.md +5 -0
  13. package/assets/patterns/browser-verification-stand.md +22 -2
  14. package/assets/patterns/doc-style-trace.md +111 -0
  15. package/assets/patterns/git-workflow-commit.github.md +1 -1
  16. package/assets/patterns/git-workflow-docker.md +203 -0
  17. package/assets/patterns/git-workflow-secrets.md +93 -0
  18. package/assets/patterns/observability-record.md +114 -0
  19. package/assets/patterns/ownership-session-procedure.md +102 -0
  20. package/assets/patterns/seo-verify.md +1 -1
  21. package/assets/patterns/spec-driven-rule.md +5 -0
  22. package/assets/patterns/styling-bem-sheet.md +178 -0
  23. package/assets/patterns/task-flow-close.md +20 -0
  24. package/assets/patterns/task-flow-resume.md +5 -0
  25. package/assets/patterns/translations-content.md +107 -0
  26. package/assets/patterns/translations-key.md +1 -1
  27. package/assets/rules/angular-patterns.md +5 -0
  28. package/assets/rules/browser-verification.md +17 -12
  29. package/assets/rules/component-structure.md +6 -2
  30. package/assets/rules/doc-style.md +16 -0
  31. package/assets/rules/git-workflow.azure.md +45 -1
  32. package/assets/rules/git-workflow.github.md +52 -1
  33. package/assets/rules/git-workflow.gitlab.md +46 -1
  34. package/assets/rules/lists.md +13 -0
  35. package/assets/rules/observability.md +147 -0
  36. package/assets/rules/ownership-scope.md +5 -2
  37. package/assets/rules/ownership-session.md +124 -0
  38. package/assets/rules/permissions.md +23 -0
  39. package/assets/rules/pricing.md +4 -0
  40. package/assets/rules/reuse-first.md +9 -0
  41. package/assets/rules/seo.md +57 -9
  42. package/assets/rules/shared-code.md +6 -0
  43. package/assets/rules/spec-driven.md +9 -0
  44. package/assets/rules/styling-bem.md +34 -1
  45. package/assets/rules/task-flow.md +5 -0
  46. package/assets/rules/testing.md +46 -8
  47. package/assets/rules/translations.md +11 -5
  48. package/assets/rules/typescript-conventions.md +5 -0
  49. package/package.json +1 -1
  50. package/rt-tools-agent-kit-0.6.0.tgz +0 -0
  51. package/rt-tools-agent-kit-0.5.3.tgz +0 -0
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: ownership-session-procedure
3
+ kind: pattern
4
+ rule: ownership-session
5
+ description: Паттерн правила ownership-session. Брать, когда процедуре нужна владеющая сущность захода — объявление декоратором, чтение из контекста, отбивка чужой сущности в черновике, контекст в тесте. Не брать для объявления доступа к процедуре — это паттерн permissions-procedure, и не для формы самого класса — это паттерн ts-procedure.
6
+ ---
7
+
8
+ # Процедура, которой нужна сущность захода
9
+
10
+ Паттерн правила `ownership-session`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/ownership.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - Процедура работает с записями владеющей сущности и должна знать, в какой она работает.
16
+ - Процедура читает идентификатор сущности из полей запроса — так больше не делают.
17
+ - Пишется тест процедуры, которая сущность захода читает.
18
+
19
+ ## Объявление и чтение
20
+
21
+ Две строки пишутся вместе: декоратор на классе и чтение из контекста в теле. Перехватчик читает
22
+ сущность только для объявивших, поэтому чтение без декоратора отвечает внутренней ошибкой — и
23
+ отвечает на первом вызове, а не на сборке.
24
+
25
+ ```ts
26
+ @Injectable()
27
+ @ConnectProcedure()
28
+ @RequiresPermission('settings:view')
29
+ @RequiresOwner()
30
+ export class GetSettingsProcedure implements IConnectProcedure<typeof SettingsContract.method.getSettings> {
31
+ public async handle(
32
+ _request: GetSettingsRequest,
33
+ context: HandlerContext
34
+ ): Promise<MessageInitShape<typeof GetSettingsResponseSchema>> {
35
+ const settings: IOwnerSettings = await this.#settings.byOwner(ownerOf(context));
36
+
37
+ }
38
+ }
39
+ ```
40
+
41
+ Запрос при этом не читается вовсе — отсюда `_request`. Поля сущности в контракте нет: номер и
42
+ имя помечены занятыми, а присланный ключ отбивается разбором запроса. Заводить его заново
43
+ нельзя: сущность в запросе называет только тот, кто работает поверх всех сразу.
44
+
45
+ Декоратор потребности стоит рядом с объявлением доступа, а не вместо него: объявлений доступа у
46
+ процедуры по-прежнему ровно одно, и без него приложение не поднимется.
47
+
48
+ ## Идентификатор, названный в теле запроса
49
+
50
+ Некоторые запросы несут сущность не отдельным полем, а внутри черновика. Расхождение с
51
+ сущностью захода отбивается, а не выправляется молча:
52
+
53
+ ```ts
54
+ const sessionOwnerId: string = ownerOf(context);
55
+ if (draft.id && draft.id !== sessionOwnerId) {
56
+ throw new ConnectError('owner does not match the session owner', Code.InvalidArgument);
57
+ }
58
+ ```
59
+
60
+ Пустой идентификатор в черновике законен: он приходит пустым, когда админка сохраняет то, чего
61
+ ещё не загружала.
62
+
63
+ ## Контекст в тесте
64
+
65
+ Двойника перехватчика заводить не надо — в контекст кладётся готовое значение:
66
+
67
+ ```ts
68
+ function context(ownerId: string): HandlerContext {
69
+ const values: ContextValues = createContextValues();
70
+ values.set(kSessionOwner, ownerId);
71
+
72
+ return { values } as unknown as HandlerContext;
73
+ }
74
+ ```
75
+
76
+ Тест на то, что присланный идентификатор не читается, пишется отдельным случаем: запрос несёт
77
+ чужую сущность, а спросили у хранилища свою.
78
+
79
+ ```ts
80
+ it('SC-OWN-54 — читает сущность захода и присланный идентификатор не смотрит', async () => {
81
+ const request: GetSettingsRequest = create(GetSettingsRequestSchema, { ownerId: FOREIGN_OWNER });
82
+
83
+ await procedure.handle(request, context(SESSION_OWNER));
84
+
85
+ expect(asked).toEqual([SESSION_OWNER]);
86
+ });
87
+ ```
88
+
89
+ ## Частые промахи
90
+
91
+ - Декоратор забыт, а чтение в теле стоит. Сборка и линт молчат, отказ приходит на первом вызове.
92
+ Ловится тестом процедуры, а не проверками дерева.
93
+ - Декоратор поставлен вместо объявления доступа. Приложение не поднимется: у процедуры обязано
94
+ быть ровно одно объявление доступа, а потребность в сущности к ним не относится.
95
+ - Сущность прочитана из запроса «на всякий случай, если контекст пуст». Пустой контекст здесь
96
+ означает забытое объявление, и подстановка прячет его до самого прода.
97
+ - Процедура читает сущность захода у гостя. Вошедшего у гостевых процедур нет, и сущности в
98
+ контексте у них тоже нет: они берут её у записи, с которой пришли, — у страницы, у разговора
99
+ по токену, — а когда записи нет вовсе, у единственной сущности хранилища. Второй в хранилище
100
+ они отвечают отказом по состоянию.
101
+ - Отказ на снятую принадлежность подан как отказ в доступе. Транспорт админки уводит на вход
102
+ только по коду неаутентифицированного, а на второй код человек остаётся на закрытом разделе.
@@ -32,7 +32,7 @@ PORT={{prodSitePort}} node dist/apps/site/server/server.mjs &
32
32
  ## Что смотреть в отданном HTML
33
33
 
34
34
  ```bash
35
- for locale in "" ru/ de/ zh-Hans/ zh-Hant/ ko/ th/ hi/; do
35
+ for locale in "" <префиксы локалей>; do
36
36
  printf '%-10s ' "${locale:-en}"
37
37
  curl -s -H "Host: localhost" "http://localhost:{{prodSitePort}}/${locale}<адрес страницы>" \
38
38
  | grep -c -E '<title>|name="description"|property="og:|rel="canonical"|hreflang=|application/ld\+json'
@@ -125,3 +125,8 @@ description: Паттерн правила git-workflow. Брать … Не б
125
125
  проверка перестанет её находить.
126
126
  - В `description` не сказано, когда паттерн **не** брать, — соседний паттерн того же правила
127
127
  становится неотличимым.
128
+ - Блок готового кода принят по виду, а не сверкой с объявлением. Вызов в примере повторяет имя,
129
+ число и порядок параметров живой функции: двухпараметрный вызов выглядел правдоподобно ровно
130
+ до того, как рядом открыли четырёхпараметрное объявление. Тем же проходом сверяется форма
131
+ кода — пример, объявляющий поля не так, как их объявляет дерево, учит нарушать правило о
132
+ языке, и запрещающее правило про это не узнает.
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: styling-bem-sheet
3
+ kind: pattern
4
+ rule: styling-bem
5
+ description: Паттерн правила styling-bem. Брать при заведении или правке шторки, окна и полноэкранного просмотра поверх страницы — чем открывается, что передаётся внутрь, как приезжает снизу, чем проверяется. Не брать для панели правки записи в админке — это паттерн entity-aside.
6
+ ---
7
+
8
+ # Шторка и окно поверх страницы
9
+
10
+ Паттерн правила `styling-bem`. Что при этом должно быть верно — закон
11
+ `docs/constitution/frontend-application.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - На телефоне заводится шторка: выбор языка, валюты, дат.
16
+ - Заводится окно или полноэкранный просмотр поверх страницы.
17
+ - Шторку или окно что-то перекрывает, и хочется поднять им `z-index`.
18
+
19
+ Панель правки записи в админке — это другое: там маршрут в своём аутлете, паттерн
20
+ `entity-aside`.
21
+
22
+ ## Открывает служба, а не разметка рядом с кнопкой
23
+
24
+ Компонент шторки — обычный элемент разметки. Он рисуется там, где написан, поэтому его
25
+ `z-index` сравнивается только с соседями по этому месту. Липкая шапка размывает фон под собой —
26
+ и всё, что написано внутри шапки, замкнуто в её слой.
27
+
28
+ Открывает шторку служба окон кита — тем же вызовом открывается полноэкранный просмотр.
29
+ Разметка уезжает к `<body>`, и сравнивать её становится не с чем.
30
+
31
+ ```typescript
32
+ readonly #dialog: RtDialogService = inject(RtDialogService);
33
+
34
+ readonly #sheetOpenedSource: Subject<RtDialogRef<string>> = new Subject<RtDialogRef<string>>();
35
+ #sheetRef: RtDialogRef<string> | null = null;
36
+
37
+ protected openLocalePicker(): void {
38
+ if (this.#sheetRef) {
39
+ return;
40
+ }
41
+
42
+ const data: ILocalePickerData = { locales: this.locales, value: this.currentLocale };
43
+ const ref: RtDialogRef<string> = this.#dialog.open<LocalePickerComponent, ILocalePickerData, string>(
44
+ LocalePickerComponent,
45
+ { data, panelClass: 'locale-picker-panel', backdropClass: 'locale-picker-backdrop' }
46
+ );
47
+ this.#sheetRef = ref;
48
+ this.#sheetOpenedSource.next(ref);
49
+ }
50
+ ```
51
+
52
+ Ответ шторки ждут подпиской из конструктора — подписываться в методе запрещает
53
+ `angular-patterns`:
54
+
55
+ ```typescript
56
+ this.#sheetOpenedSource
57
+ .pipe(
58
+ mergeMap((ref: RtDialogRef<string>): Observable<string | undefined> => ref.afterClosed()),
59
+ takeUntilDestroyed(this.#destroyRef)
60
+ )
61
+ .subscribe((code: string | undefined): void => {
62
+ this.#sheetRef = null;
63
+ if (code) {
64
+ this.onLocaleChange(code);
65
+ }
66
+ });
67
+ ```
68
+
69
+ Ссылку обнуляют здесь, а не в обработчике кнопки: шторку закрывают ещё жестом вниз и нажатием
70
+ мимо, и оба пути идут мимо кнопки.
71
+
72
+ ## Внутри шторки — только она сама
73
+
74
+ Что показать, приходит токеном. Что выбрали, уходит вместе с закрытием. Своей кнопки у шторки
75
+ нет: кнопку держит тот, кто шторку открывает.
76
+
77
+ ```typescript
78
+ export interface ILocalePickerData {
79
+ readonly locales: readonly ISiteLocale[];
80
+ readonly value: string;
81
+ }
82
+
83
+ readonly #data: ILocalePickerData = inject<ILocalePickerData>(RT_DIALOG_DATA);
84
+ readonly #dialogRef: RtDialogRef<string> = inject<RtDialogRef<string>>(RtDialogRef);
85
+
86
+ protected confirm(): void {
87
+ this.#dialogRef.close(this.centeredLocale());
88
+ }
89
+
90
+ /** Закрыли, не подтвердив: значит, передумали — значение не меняем */
91
+ protected onOpenChange(open: boolean): void {
92
+ this.open.set(open);
93
+ if (!open) {
94
+ this.#dialogRef.close();
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## Шторка приезжает снизу
100
+
101
+ Служба поднимает её сразу открытой, и анимировать киту нечего: вместо движения гость видит
102
+ подмену экрана. Поэтому открывают её на кадр позже:
103
+
104
+ ```typescript
105
+ protected readonly open: WritableSignal<boolean> = signal<boolean>(false);
106
+
107
+ constructor() {
108
+ afterNextRender((): void => {
109
+ this.open.set(true);
110
+ });
111
+ }
112
+ ```
113
+
114
+ `afterNextRender`, а не `ngAfterViewInit`: страницу отдаёт сервер, и разметка появляется позже.
115
+
116
+ ## Подложка
117
+
118
+ Шторка сама затемняет фон и сама ловит нажатие мимо. Подложка окна поверх неё дала бы второе
119
+ затемнение — фон стал бы вдвое темнее, чем у соседней шторки. Поэтому её делают прозрачной.
120
+ Правило пишут в общий слой приложения: шторка уехала к `<body>`, и файл стилей компонента до
121
+ неё не достаёт.
122
+
123
+ ```scss
124
+ /* общий слой приложения; имя класса несёт приставку дерева */
125
+ .locale-picker-backdrop {
126
+ background: transparent;
127
+ }
128
+ ```
129
+
130
+ Нажатие она ловит по-прежнему — закрытие по ней делает само окно.
131
+
132
+ ## Чем проверяется
133
+
134
+ Скриншот тут не поможет: перекрытая шторка выглядит целой, а сборка и линт молчат. Спрашивают
135
+ точку — кому достанется нажатие:
136
+
137
+ ```javascript
138
+ const rect = button.getBoundingClientRect();
139
+ document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
140
+ ```
141
+
142
+ Ответом должна быть сама кнопка. Мерить надо на стенде из прод-сборки и на узком экране —
143
+ паттерн `browser-verification-measure`.
144
+
145
+ В сквозном тесте перед замером дожидаются, пока шторка доедет: видимая кнопка ещё может
146
+ двигаться, и координаты через кадр будут другими.
147
+
148
+ ```typescript
149
+ await page.locator('[qa-dataid="locale-picker-sheet"] [qa-dataid="bottom-sheet-panel"]').evaluate(
150
+ (panel: Element): Promise<void> =>
151
+ new Promise<void>((resolve: () => void): void => {
152
+ if (panel.getBoundingClientRect().bottom <= window.innerHeight) {
153
+ resolve();
154
+
155
+ return;
156
+ }
157
+ panel.addEventListener('transitionend', (): void => resolve(), { once: true });
158
+ })
159
+ );
160
+ ```
161
+
162
+ ## Частые промахи
163
+
164
+ - **Шторка написана рядом со своей кнопкой в шапке.** Шапка размывает фон, и шторка замкнута в
165
+ её слой. Так нижняя панель накрыла шторку вместе с кнопкой подтверждения, и значение на
166
+ телефоне стало не сменить.
167
+ - **Шапке подняли номер слоя.** Дефект уходит, шторка остаётся замкнутой в чужой слой:
168
+ следующий сосед с номером повыше накроет её снова.
169
+ - **Шторку перенесли в другое место разметки.** То же самое: заработает, пока над новым местом
170
+ никто не поставит `transform`, `filter` или свой `z-index`.
171
+ - **Правило подложки положили в стили компонента.** Подложка уехала к `<body>`, и правило до
172
+ неё не достаёт — писать надо в общий слой приложения.
173
+ - **Ссылку на открытую шторку обнулили в обработчике кнопки.** Закрытие жестом и нажатием мимо
174
+ идёт мимо него, и второй раз шторка уже не откроется.
175
+ - **Тест померил сразу после проверки видимости.** Шторка ещё едет, `elementFromPoint`
176
+ возвращает `null`, и тест краснеет на исправном коде.
177
+ - **Образец искали по именам библиотеки, поверх которой собран кит.** Она лежит внутри кита, её
178
+ имён в дереве нет — искать надо по именам кита.
@@ -148,7 +148,27 @@ npm run check:docs # пути, названные в текстах, суще
148
148
  выкачено» с тем, что работает месяц, — беззвучная ложь, тем убедительнее, чем старше.
149
149
  - **Номера сценариев при вливании не пересчитываются.** Идентификатор — ключ связи с тестами;
150
150
  сдвиг рвёт сверку у соседей, которых правка не касалась.
151
+ - **«Что не входит» после вливания читается целиком, а не дописывается.** Договорённость несёт
152
+ свои границы, и при вливании они ложатся рядом с границами домена: половина повторяет уже
153
+ стоявшее другими словами, а строка «этого раздела ещё нет» становится ложью ровно той
154
+ работой, которая её вливает. Сверка спеков в этот раздел не смотрит вовсе. Снимается
155
+ дословный повтор и то, что работа сделала входящим.
156
+ - **Шаги закрытия с владельцем не согласуются — они перечислены здесь.** Разбор работы
157
+ правилами входит в закрытие так же, как вливание договорённости и разбор папки; владелец
158
+ решает не то, запускать ли его, а что делать с находками. Ход, кончившийся таким вопросом,
159
+ отбивает гард разговора: за ход правила не читались, а ответ стоит в них. Спрашивается только
160
+ то, чего в правилах нет.
161
+ - **Блок готового кода в паттерне стареет от чужой правки.** Он не привязан ни к чему: сверка
162
+ спеков читает утверждения правила, а пример под ними не читает вовсе. Два поля, ставших
163
+ обязательными в чужой работе, сделали пример в соседнем паттерне несобираемым — сам он при
164
+ этом не изменился ни на знак и в след задачи не попал, потому что ни одного слова той работы
165
+ в нём нет. Паттерн находится по имени правленого символа, а не по теме работы.
151
166
  - **Правило без привязки в спек домена не въезжает.** Кода, который его исполняет, нет —
152
167
  значит это намерение, и место ему в открытых вопросах домена, а не в правилах.
168
+ - **Замысел линии работ правят только там, где вписывают «чем кончилась».** Границы линии и
169
+ порядок задач в ней при этом остаются прежними, а работа их уже нарушила: задача, решившая
170
+ читать спеки, оставила над собой границу «спеки — вторая очередь», и следующий исполнитель
171
+ прочитает её как действующую. Границы линии перечитываются целиком тем же заходом, что и
172
+ итог работы.
153
173
  - **Архив не обновляется после выкатки.** Уехавшее туда описывает день переезда, и правится
154
174
  оно только вместе с признанием, что описывало неверно.
@@ -102,5 +102,10 @@ git log --oneline origin/main..HEAD
102
102
  несохранённым и почему» — единственное, по чему это видно, пока отчёта нет.
103
103
  - **Подтверждение — вывод команды или замер, а не пересказ.** «Проверил, работает» через
104
104
  заход неотличимо от «казалось, что работает».
105
+ - **Число из передачи пересчитывается до того, как на нём что-то делят.** Передача описывает
106
+ день, когда её писали, лежит вне дерева, и не читает её ни одна проверка. Оценка «работы
107
+ вдвое больше предыдущей» при пересчёте на текущем коммите не подтвердилась: по числу
108
+ утверждений объёмы оказались почти равны. Деление работы по чужой оценке делит не то, и
109
+ владельцу называются свои числа.
105
110
  - **Доэтапное отделяется от своего.** Красное, найденное по дороге и не этой работой
106
111
  сделанное, помечается таковым сразу: иначе следующий заход примет его за свою поломку.
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: translations-content
3
+ kind: pattern
4
+ rule: translations
5
+ description: Паттерн правила translations. Брать при работе с переводами содержимого записи — производный перевод при сохранении, запись локали руками через язык админки, разбор отказа провайдера, сброс кэша отданных страниц. Не брать для подписей интерфейса — это паттерн translations-key.
6
+ ---
7
+
8
+ # Перевод содержимого записи
9
+
10
+ Паттерн правила `translations`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/locales.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - Владелец правит тексты записи, и остальные локали должны догнать.
16
+ - Админка ответила, что сохранила на языке ввода, а перевод на остальные не удался.
17
+ - Локаль надо заполнить руками: провайдер недоступен, а страница на этом языке нужна сегодня.
18
+ - Проверяется, что отданная страница показывает свежий текст.
19
+
20
+ ## Перевод идёт сам, и просить его нечем
21
+
22
+ Кнопки «перевести» нет. Владелец печатает на своём языке, а процедура сохранения зовёт перевод
23
+ до транзакции и кладёт в хранилище уже все локали. Локаль ввода приезжает полем запроса и
24
+ берётся из языка админки, а не из браузера гостя.
25
+
26
+ В партию идёт не всё подряд: сборщик отбрасывает строку, чей исходный текст не менялся **и**
27
+ которая переведена на все целевые локали. Отсюда два следствия:
28
+
29
+ - сохранение записи без правки текстов провайдера не зовёт вовсе — перестановка изображения или
30
+ правка числа ничего не стоит;
31
+ - строка, у которой хоть одна локаль пуста, поедет на перевод при первом же сохранении, без
32
+ правки самого текста.
33
+
34
+ ## Локаль пишется руками через язык админки
35
+
36
+ Табов локалей в форме нет, и это не упущение: поле одно, а какая локаль за ним стоит, решает
37
+ язык интерфейса. Значит записать перевод руками — это переключить язык в шапке админки и
38
+ заполнить те же поля.
39
+
40
+ Путь этот не аварийный: пока у провайдера пуст счёт, он и есть обычный порядок, и каждый
41
+ видимый текст заводится по разу на локаль. Считать его временным и ждать автоперевода не надо.
42
+
43
+ Порядок такой:
44
+
45
+ 1. Меню владельца в шапке → выбрать язык.
46
+ 2. Открыть запись. Текстовые поля покажут текст **этой** локали; пустые они там, где локали у
47
+ записи нет.
48
+ 3. Заполнить и сохранить.
49
+
50
+ Соседние локали при этом целы: черновик пишет в карту по коду локали, а остальные ключи уезжают
51
+ в запрос как пришли. Отказ провайдера ничего не портит — в хранилище ложится ровно то, что
52
+ прислала форма.
53
+
54
+ Записанное руками потом не перетирается: у поля заполнены все локали, и сборщик партии больше
55
+ не берёт его, пока владелец не изменит исходный текст.
56
+
57
+ Собирать вызов сохранения скриптом для этого не надо, и правило это прямо запрещает: черновик
58
+ перезаписывает запись целиком, а вложенная запись, не попавшая в список, удаляется вместе со
59
+ своими связями.
60
+
61
+ ## Отказ провайдера читается не по экрану
62
+
63
+ Владельцу показывается одна фраза без причины. Причина живёт в логах строкой своего контекста:
64
+
65
+ ```bash
66
+ docker compose -f <состав прода> --env-file <файл окружения> logs api --since 30m \
67
+ | grep -i translation
68
+ ```
69
+
70
+ Строка несёт код ответа, но не текст: клиент бросает отказ по коду, не читая тела. Что именно
71
+ ответил провайдер, спрашивается у него напрямую тем же ключом; для разового вызова его берут
72
+ руками — приложение эту переменную не читает, но значение там то же, что владелец завёл в
73
+ админке. Отказ по деньгам означает пустой счёт, а не сломанный ключ: список моделей при этом
74
+ отдаётся, и строка интеграции показывает «работает». Где лежит сам ключ и почему проба
75
+ зелёная — паттерн `git-workflow-secrets`.
76
+
77
+ ## Отданная страница показывает прежний текст, пока не сброшен кэш
78
+
79
+ Записанный перевод виден в хранилище сразу, а гостю — нет: страницы лежат в кэше прокси. На
80
+ проде он сбрасывается так же, как на стенде:
81
+
82
+ ```bash
83
+ docker exec <контейнер прокси> sh -c 'rm -rf /var/cache/nginx/site/*; nginx -s reload'
84
+ ```
85
+
86
+ Сверяется отданной разметкой по каждой правленой локали, а не взглядом на админку:
87
+
88
+ ```bash
89
+ for l in <локали>; do
90
+ printf '%-8s ' "$l"
91
+ curl -sL "https://<адрес сайта>/$l/" | grep -oE '<meta name="description" content="[^"]{0,80}'
92
+ done
93
+ ```
94
+
95
+ ## Частые промахи
96
+
97
+ - **Проверять результат в админке.** Форма показывает то, что в хранилище, и молчит о кэше: три
98
+ локали были записаны и проверены, а сайт ещё полчаса отдавал прежний текст на всех.
99
+ - **Считать, что перевода нет, раз пришло предупреждение.** Прежние локали остаются на месте;
100
+ пустой окажется только та, которой не было раньше.
101
+ - **Писать в хранилище напрямую.** Запись в боевое хранилище запрещена совсем, а колонка со
102
+ свободной структурой выглядит безобидной целью — тексты правятся через админку, и другого
103
+ пути у них нет.
104
+ - **Переводить имя собственное.** Оно остаётся как есть во всех локалях: так велит словарь в
105
+ задании провайдеру, и рукописный перевод обязан вести себя так же.
106
+ - **Копировать одно письмо языка в другое.** Упрощённое и традиционное письмо — разные локали и
107
+ разное письмо; посимвольная копия простояла на проде, и гость читал чужие иероглифы.
@@ -27,7 +27,7 @@ description: Паттерн правила translations. Брать, когда
27
27
  | `admin.json` | админ-панель |
28
28
  | `mail.json` | письма и документ-основание |
29
29
 
30
- Локалей восемь: `en`, `ru`, `de`, `zh-Hans`, `zh-Hant`, `ko`, `th`, `hi`. Ключ заводится во
30
+ Набор локалей перевода назван в `implementation.md` правила. Ключ заводится во
31
31
  всех — пустое значение считается пропуском, а не переводом.
32
32
 
33
33
  ## Подстановка
@@ -40,6 +40,11 @@ description: Правило под «Закон о фронтовом прило
40
40
  объявлена подписка.
41
41
  - **Источник действия носит суффикс `Source` в имени.** Иначе поток и значение в коде
42
42
  неотличимы, и `next` уходит не туда.
43
+ - **Вызов сервиса внутри `computed` зависимостью не становится.** Производное значение следит
44
+ только за прочитанными сигналами, а обычный метод сигналом не является: значение остаётся
45
+ таким, каким было в момент первого счёта. Текущий язык, текущее владение, текущий признак
46
+ среды читаются сигналом службы — тогда производное пересобирается вместе с их сменой. Ни
47
+ сборка, ни линтер этого не видят.
43
48
  - **Списочный стор наследует общую основу.** Записи, страница, порядок, условия отбора, строка
44
49
  поиска и конфиг выборки уже там, и наследнику остаются четыре строки.
45
50
 
@@ -2,7 +2,7 @@
2
2
  name: browser-verification
3
3
  kind: rule
4
4
  law: verifiability
5
- description: Правило под «Закон о проверяемости». Брать при любой проверке через браузер и при запросах curl или wget к дев-серверу. Называет порты сайта, админки и API, чему на дев-сервере верить нельзя и чем измерять вместо взгляда. Готовый код — в паттернах browser-verification-stand и browser-verification-measure.
5
+ description: Правило под «Закон о проверяемости». Брать при любой проверке через браузер и при запросах curl или wget к дев-серверу. Называет, где подняты приложения дерева, чему на дев-сервере верить нельзя и чем измерять вместо взгляда. Готовый код — в паттернах browser-verification-stand и browser-verification-measure.
6
6
  ---
7
7
 
8
8
  # Проверка работающего приложения — как это устроено здесь
@@ -16,7 +16,7 @@ description: Правило под «Закон о проверяемости».
16
16
  | В законе | Здесь |
17
17
  | --------------------------------- | ----------------------------------------------------------------------------------------------- |
18
18
  | работающее приложение | то, что поднято в этом дереве; перечень и порты — в `implementation.md` рядом |
19
- | место, где его видит пользователь | прод-сборка за настоящим `deploy/nginx.conf`, а не дев-сервер |
19
+ | место, где его видит пользователь | прод-сборка за настоящим прокси дерева, а не дев-сервер |
20
20
  | замер | `getComputedStyle`, `getBoundingClientRect`, контраст, совпадение центров, попадание во вьюпорт |
21
21
  | драйвер браузера | `claude-in-chrome` на закреплённом профиле этого дерева |
22
22
 
@@ -39,6 +39,11 @@ description: Правило под «Закон о проверяемости».
39
39
  имена, которые не опознают ничего, а выбор из него ведёт на профиль без входа.
40
40
  - **Прод-конфигурация проверяется только за настоящим прокси.** Голый сервер отдачи страниц
41
41
  про кэш, перенаправления и заголовки не знает ничего.
42
+ - **Первый заход на публичный экран метится признаком служебного посещения.** Драйвер водит
43
+ обычный браузер, и счётчик посещений не отличает проверку от гостя: `navigator.webdriver` у
44
+ него `false`, строка `User-Agent` — живого браузера. Чем метится заход, сказано в именах
45
+ дерева; признак, который живёт в хранилище браузера, дописывается один раз на профиль, а не
46
+ к каждому адресу.
42
47
 
43
48
  Вывод о вёрстке подкрепляется числом: «выглядит нормально» результатом проверки не является.
44
49
  Этого не стережёт ничто — как измерять, разобрано в паттерне `browser-verification-measure`.
@@ -50,21 +55,21 @@ description: Правило под «Закон о проверяемости».
50
55
 
51
56
  ## Паттерны
52
57
 
53
- - `browser-verification-stand` — честный стенд из прод-сборки, вход в админку, разбор порта.
58
+ - `browser-verification-stand` — честный стенд из прод-сборки, вход в закрытое приложение,
59
+ разбор порта.
54
60
  - `browser-verification-measure` — замер вместо взгляда, ловушки инструмента `computer`.
55
61
 
56
62
  ## Ловушки
57
63
 
58
64
  - **Сначала выяснить, что отвечает на порту:** `lsof -nP -iTCP:<порт> -sTCP:LISTEN` до первого
59
- запроса. На 3333 регулярно висит собранный артефакт из прошлой сессии — он отвечает 200
60
- старым кодом, а заведённой в ветке процедуры у него нет вовсе, и 404 читается как дефект
61
- регистрации. Таких процессов бывает несколько; `pkill` по `nx serve api` не попадает ни в
62
- один — убивать по PID из `lsof`, каждый.
63
- - Инкрементальная сборка протухает поштучно: разметка на 4900 бывает уже новая, а клиентский
64
- чанк — от компиляции до правки. Признак дев-сборки — имена бандла без хеша (`main.js`).
65
- Расхождение между `curl` и страницей после гидратации — повод пересобрать, а не искать
66
- дефект в коде. Отсюда же нельзя делать вывод «такого маршрута нет»: сверяться с
67
- `app.routes.ts`.
65
+ запроса. На порту приложения регулярно висит собранный артефакт из прошлой сессии — он
66
+ отвечает 200 старым кодом, а заведённого в ветке обработчика у него нет вовсе, и 404 читается
67
+ как дефект регистрации. Таких процессов бывает несколько; снятие по шаблону команды не
68
+ попадает ни в один — убивать по PID из `lsof`, каждый.
69
+ - Инкрементальная сборка протухает поштучно: разметка бывает уже новая, а клиентский чанк — от
70
+ компиляции до правки. Признак дев-сборки — имена бандла без хеша (`main.js`). Расхождение
71
+ между `curl` и страницей после гидратации — повод пересобрать, а не искать дефект в коде.
72
+ Отсюда же нельзя делать вывод «такого маршрута нет»: сверяться с объявлением маршрутов.
68
73
  - **Кэш объясняет расхождение, но не подтверждает его.** В `.angular/cache/…/vite/deps` лежат
69
74
  только пакеты из `node_modules`, кода репозитория там нет вовсе. Вывод «дефекта нет, это
70
75
  кэш» закрывает разбор, поэтому принимается только после проверки на чистой сборке — три
@@ -16,10 +16,10 @@ description: Правило под «Закон о фронтовом прило
16
16
 
17
17
  | В законе | Здесь |
18
18
  | ---------------------------------- | ---------------------------------------------------------------------------- |
19
- | компонент | `vm-<имя>` — префикс один на сайт и админку |
19
+ | компонент | `<префикс>-<имя>` — префикс один на все приложения дерева |
20
20
  | готовое, а не вычисление в шаблоне | `computed()`; там, где значение приходит из контекста шаблона, — чистый пайп |
21
21
  | якорь для проверки | атрибут `qa-dataid` в kebab-case по смыслу элемента |
22
- | корень разметки | `:host` с классом блока от `host: { class: 'vm-<имя>' }` |
22
+ | корень разметки | `:host` с классом блока от `host: { class: '<префикс>-<имя>' }` |
23
23
 
24
24
  ## Где это лежит
25
25
 
@@ -33,6 +33,10 @@ description: Правило под «Закон о фронтовом прило
33
33
  (computeFlag())`, чтения сигналов не трогает.
34
34
  - **Каждый интерактивный элемент несёт `qa-dataid`.** Это единственный якорь спек: классы BEM
35
35
  меняются вместе с вёрсткой, а поиск по роли и тексту ломается на локалях перевода.
36
+ - **Компоненту разрешён только элементный селектор.** Правило линтера требует у компонента
37
+ элемент с приставкой дерева и именем через дефис, у директивы — атрибут и имя одним словом.
38
+ Приём, который вешается на чужой тег, пишется директивой с самого начала: у компонента с
39
+ селектором-атрибутом линт краснеет уже после того, как написаны все три файла и стили.
36
40
  - **Класс блока висит на хосте, а не на обёртке внутри шаблона.** Лишняя обёртка вокруг всех
37
41
  детей — это раскладка, и ей место на `:host`.
38
42
 
@@ -56,6 +56,20 @@ description: Правило под «Закон о документации пр
56
56
  имена веток и правила линтеров: выглядят адресом, адресом не являются.
57
57
  - **Документ едет в том же коммите, что и правка, которую он описывает.** Обход — строка
58
58
  `Docs-skip: <причина>` в теле коммита; пустая причина не принимается.
59
+ - **Документ не длиннее предела длины.** Текст, который не влезает на экран целиком, дописывают
60
+ в конец, не перечитав начала, — так в одном документе и оказываются два ответа на один вопрос.
61
+ Предел тот же, что у кода, и считается так же — все строки; выросший спек делится на
62
+ поддомены, а не переносит границу. Описание прошлого из счёта выведено: архив по устройству
63
+ перечисляет то, чего в дереве уже нет, а папка задачи умирает со слиянием.
64
+ - **Файл, уезжающий в описание прошлого, называет в шапке свой прежний адрес.** Записи архива
65
+ ссылались на него, пока он был живым, и после переезда эти ссылки ведут в пустоту: проверка
66
+ путей архив не читает вовсе, поэтому промах не краснеет никогда. Найти переехавшее нечем —
67
+ имя записи архива с прежним адресом не совпадает, и поиск по нему её не показывает. Одна
68
+ строка в шапке дешевле правки всех ссылающихся записей и прошлого не трогает.
69
+ - **Текст, называющий состояние машины, устаревает без единой правки в дереве.** Ловушка о том,
70
+ что на машине установлено, верна в день, когда её пишут, и становится неправдой сама собой —
71
+ ни одна сверка этого не видит: они читают дерево, а состарилась машина. Утверждение о машине
72
+ пишется способом её спросить: команда и то, с чем сверять ответ, вместо снимка ответа.
59
73
 
60
74
  ## Чего из закона здесь нет
61
75
 
@@ -74,6 +88,8 @@ description: Правило под «Закон о документации пр
74
88
 
75
89
  - `doc-style-write` — как формулировать: примеры «так» и «не так», правила для комментариев.
76
90
  - `doc-style-sweep` — разбор документа, накопившего список работ, на действующее и закрытое.
91
+ - `doc-style-trace` — обратный проход: закрытые задачи против текстов, поиск того, чего не
92
+ написали.
77
93
 
78
94
  ## Скилы дерева
79
95