@rt-tools/agent-kit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +235 -18
- package/assets/agents/business-analyst.md +74 -0
- package/assets/agents/project-manager.md +70 -0
- package/assets/agents/qa-engineer.md +72 -0
- package/assets/agents/skill-curator.md +110 -0
- package/assets/agents/spec-critic.md +44 -0
- package/assets/agents/spec-writer.md +50 -0
- package/assets/checks/board.github.mjs +286 -0
- package/assets/checks/check-board.github.mjs +188 -0
- package/assets/checks/check-doc-paths.mjs +163 -0
- package/assets/checks/check-dupes.mjs +277 -0
- package/assets/checks/check-lib-layers.mjs +573 -0
- package/assets/checks/check-reuse.mjs +208 -0
- package/assets/checks/check-schema-drift.mjs +186 -0
- package/assets/checks/check-specs.mjs +1007 -0
- package/assets/checks/check-styles.mjs +109 -0
- package/assets/checks/rt-kit-checks.config.mjs +134 -0
- package/assets/checks/task-new.github.mjs +198 -0
- package/assets/commands/skill-curator.md +70 -0
- package/assets/defaults/gate-map.sh +100 -0
- package/assets/defaults/project.sh +179 -0
- package/assets/hooks/browser-device-id.sh +20 -0
- package/assets/hooks/browser-guard-device-id.sh +28 -0
- package/assets/hooks/browser-guard-no-asking.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +18 -0
- package/assets/hooks/browser-guard-no-other-drivers.sh +79 -0
- package/assets/hooks/browser-guard-require-select.sh +54 -0
- package/assets/hooks/commit-msg.sh +26 -0
- package/assets/hooks/constitution-index.sh +43 -0
- package/assets/hooks/dev-server-guard.sh +115 -0
- package/assets/hooks/docs-guard.sh +282 -0
- package/assets/hooks/git-guard-delivery.sh +167 -0
- package/assets/hooks/git-guard-main.sh +73 -0
- package/assets/hooks/git-guard-push-tests.sh +94 -0
- package/assets/hooks/glossary-load.sh +23 -0
- package/assets/hooks/lint-after-edit.sh +219 -0
- package/assets/hooks/qa-dataid-guard.sh +121 -0
- package/assets/hooks/reuse-first-guard.sh +154 -0
- package/assets/hooks/skill-gate-rearm.sh +23 -0
- package/assets/hooks/skill-gate.sh +128 -0
- package/assets/hooks/skill-loaded.sh +21 -0
- package/assets/hooks/sql-guard.sh +679 -0
- package/assets/hooks/task-context-load.sh +100 -0
- package/assets/hooks/task-flow-guard.sh +107 -0
- package/assets/laws/{access.md → application/access.md} +1 -4
- package/assets/laws/{locales.md → application/locales.md} +1 -3
- package/assets/laws/application/money.md +41 -0
- package/assets/laws/application/ownership.md +32 -0
- package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
- package/assets/laws/code-structure.md +7 -6
- package/assets/laws/delivery.md +53 -3
- package/assets/laws/entity-editing.md +49 -55
- package/assets/laws/entity-models.md +4 -14
- package/assets/laws/frontend-application.md +5 -5
- package/assets/laws/lib-imports.md +14 -1
- package/assets/laws/lists.md +33 -0
- package/assets/laws/navigation.md +40 -0
- package/assets/laws/project-documentation.md +17 -8
- package/assets/laws/reuse-first.md +26 -21
- package/assets/laws/shared-code.md +13 -1
- package/assets/laws/verifiability.md +17 -1
- package/assets/laws/work-conduct.md +48 -0
- package/assets/patterns/admin-lists-screen.md +131 -0
- package/assets/patterns/admin-nav-item.md +71 -0
- package/assets/patterns/angular-patterns-state.md +101 -0
- package/assets/patterns/api-layer-pair.md +88 -0
- package/assets/patterns/browser-verification-measure.md +86 -0
- package/assets/patterns/browser-verification-stand.md +143 -0
- package/assets/patterns/component-structure-new.md +99 -0
- package/assets/patterns/dependencies-upgrade.md +65 -0
- package/assets/patterns/doc-style-sweep.md +137 -0
- package/assets/patterns/doc-style-write.md +109 -0
- package/assets/patterns/entity-aside.md +136 -0
- package/assets/patterns/entity-models-new.md +124 -0
- package/assets/patterns/entity-store.md +91 -0
- package/assets/patterns/git-workflow-commit.azure.md +259 -0
- package/assets/patterns/git-workflow-commit.github.md +333 -0
- package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
- package/assets/patterns/git-workflow-merge.md +99 -0
- package/assets/patterns/git-workflow-migration.md +88 -0
- package/assets/patterns/git-workflow-restart.md +49 -0
- package/assets/patterns/lib-layers-move.md +95 -0
- package/assets/patterns/lib-layers-new.md +82 -0
- package/assets/patterns/ownership-scope-resolve.md +69 -0
- package/assets/patterns/permissions-procedure.md +71 -0
- package/assets/patterns/platform-access-di.md +84 -0
- package/assets/patterns/pricing-quote.md +71 -0
- package/assets/patterns/reuse-first-extend.md +73 -0
- package/assets/patterns/seo-page.md +104 -0
- package/assets/patterns/seo-verify.md +83 -0
- package/assets/patterns/shared-code-new.md +86 -0
- package/assets/patterns/spec-driven-domain.md +107 -0
- package/assets/patterns/spec-driven-rule.md +127 -0
- package/assets/patterns/styling-bem-component.md +88 -0
- package/assets/patterns/styling-bem-layout.md +73 -0
- package/assets/patterns/task-flow-close.md +90 -0
- package/assets/patterns/task-flow-resume.md +94 -0
- package/assets/patterns/task-flow-start.md +117 -0
- package/assets/patterns/testing-e2e.md +92 -0
- package/assets/patterns/testing-unit.md +117 -0
- package/assets/patterns/translations-key.md +64 -0
- package/assets/patterns/ts-procedure.md +65 -0
- package/assets/rules/angular-patterns.md +71 -0
- package/assets/rules/api-layer.md +71 -0
- package/assets/rules/browser-verification.md +87 -0
- package/assets/rules/component-structure.md +64 -0
- package/assets/rules/dependencies.md +66 -0
- package/assets/rules/doc-style.md +103 -0
- package/assets/rules/entity-conventions.md +78 -0
- package/assets/rules/entity-models.md +70 -0
- package/assets/rules/git-workflow.azure.md +116 -0
- package/assets/rules/git-workflow.github.md +123 -0
- package/assets/rules/git-workflow.gitlab.md +113 -0
- package/assets/rules/lib-layers.md +80 -0
- package/assets/rules/lists.md +73 -0
- package/assets/rules/navigation.md +78 -0
- package/assets/rules/ownership-scope.md +63 -0
- package/assets/rules/permissions.md +70 -0
- package/assets/rules/platform-access.md +77 -0
- package/assets/rules/pricing.md +64 -0
- package/assets/rules/reuse-first.md +83 -0
- package/assets/rules/seo.md +71 -0
- package/assets/rules/shared-code.md +70 -0
- package/assets/rules/spec-driven.md +135 -0
- package/assets/rules/styling-bem.md +74 -0
- package/assets/rules/task-flow.md +110 -0
- package/assets/rules/testing.md +100 -0
- package/assets/rules/translations.md +69 -0
- package/assets/rules/typescript-conventions.md +76 -0
- package/assets/skills/agent-kit.md +81 -0
- package/assets/skills/write-a-skill.md +108 -0
- package/assets/templates/gate-map.sh +45 -0
- package/assets/templates/implementation.md +44 -0
- package/assets/templates/pattern.md +5 -1
- package/assets/templates/project.sh +54 -0
- package/assets/templates/rule.md +12 -23
- package/assets/variants.json +20 -0
- package/assets/workflows/feature.js +134 -0
- package/assets/workflows/plan.js +150 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +78 -5
- package/bin/agent-kit.js.map +1 -1
- package/bin/prompt.d.ts +5 -0
- package/bin/prompt.d.ts.map +1 -1
- package/bin/prompt.js +19 -7
- package/bin/prompt.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/assets.d.ts +14 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +23 -2
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +52 -5
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +104 -16
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +22 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +218 -11
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +57 -0
- package/lib/companion.d.ts.map +1 -0
- package/lib/companion.js +60 -0
- package/lib/companion.js.map +1 -0
- package/lib/config.d.ts +42 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +60 -2
- package/lib/config.js.map +1 -1
- package/lib/freshness.d.ts +14 -0
- package/lib/freshness.d.ts.map +1 -0
- package/lib/freshness.js +116 -0
- package/lib/freshness.js.map +1 -0
- package/lib/hooks-map.d.ts +24 -0
- package/lib/hooks-map.d.ts.map +1 -0
- package/lib/hooks-map.js +72 -0
- package/lib/hooks-map.js.map +1 -0
- package/lib/integrity.d.ts +36 -0
- package/lib/integrity.d.ts.map +1 -0
- package/lib/integrity.js +44 -0
- package/lib/integrity.js.map +1 -0
- package/lib/picker.d.ts +11 -1
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +44 -6
- package/lib/picker.js.map +1 -1
- package/lib/stamp.d.ts +2 -5
- package/lib/stamp.d.ts.map +1 -1
- package/lib/stamp.js +25 -10
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +29 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +78 -4
- package/lib/sync.js.map +1 -1
- package/lib/variants.d.ts +44 -0
- package/lib/variants.d.ts.map +1 -0
- package/lib/variants.js +82 -0
- package/lib/variants.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.4.0.tgz +0 -0
- package/assets/laws/admin-lists.md +0 -35
- package/assets/laws/admin-navigation.md +0 -38
- package/rt-tools-agent-kit-0.2.0.tgz +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-style-sweep
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: doc-style
|
|
5
|
+
description: Паттерн правила doc-style. Брать, когда документ накопил список работ и его надо разобрать на действующее и закрытое — признак отбора, обход по утверждениям, сверка с очередью работ, измерение потерь, куда девать доводы за отсрочку и судьба самого файла. Не брать для написания нового текста — это паттерн doc-style-write.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Разбор документа, накопившего список работ
|
|
9
|
+
|
|
10
|
+
Паттерн правила `doc-style`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/project-documentation.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
|
+
```bash
|
|
43
|
+
# сколько чего в файле: пункты, абзацы, разделы
|
|
44
|
+
grep -c '^- ' docs/BACKLOG.md
|
|
45
|
+
grep -c '^## ' docs/BACKLOG.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Абзац судится тем же признаком, что и пункт.
|
|
49
|
+
|
|
50
|
+
## Номер задачи стоит в трёх местах
|
|
51
|
+
|
|
52
|
+
Прежде чем считать «пункты без задачи», надо знать все формы записи. В этом дереве их три:
|
|
53
|
+
|
|
54
|
+
```markdown
|
|
55
|
+
## Раздел — #149 ← в заголовке
|
|
56
|
+
|
|
57
|
+
**Тикеты:** #163, #164 ← отдельной строкой у раздела или подраздела
|
|
58
|
+
|
|
59
|
+
- Пункт про дефект. #101 ← в конце пункта
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Разбор, знающий одну форму, ошибается молча: «51 пункт без задачи» оказался шестью. **Число,
|
|
63
|
+
полученное разбором текста, сверяется на выборке руками до того, как его называют.**
|
|
64
|
+
|
|
65
|
+
## Сверка с очередью работ
|
|
66
|
+
|
|
67
|
+
У пункта есть задача — это ещё не значит, что задача несёт его содержание. Перед удалением
|
|
68
|
+
меряется покрытие: сколько значимых слов пункта встречается в теле его задачи.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
/opt/homebrew/bin/gh issue list --state all --limit 400 --json number,title,body,state > /tmp/issues.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Пункт, покрытый телом наполовину и меньше, читается глазами и разводится на три исхода:
|
|
75
|
+
|
|
76
|
+
- живое уточнение, которого в задаче нет, — дописать в тело;
|
|
77
|
+
- другой дефект — завести своей задачей;
|
|
78
|
+
- протухшее — удалить, **не** перенося. Иначе разбор занесёт в задачу ложь: «гард не проверяет
|
|
79
|
+
каталог кита» переносить было некуда, каталога уже не существовало.
|
|
80
|
+
|
|
81
|
+
**Наследование номера от заголовка — предположение, а не факт.** Пункт под заголовком с
|
|
82
|
+
четырьмя номерами не принадлежит ни одному из них: привязка проверяется чтением.
|
|
83
|
+
|
|
84
|
+
## Сделанность читается по дереву
|
|
85
|
+
|
|
86
|
+
Утверждение о том, что задача не сделана, стареет так же, как любое другое. Дважды за один
|
|
87
|
+
разбор устаревшее было названо действующим: маркер непросмотренного уже вёз кит, а половина
|
|
88
|
+
задачи про гейт скилов была сделана и покрыта сценариями.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# проверять то, о чём собираешься сказать «не сделано»
|
|
92
|
+
grep -rn '<символ>' libs apps .claude/hooks
|
|
93
|
+
grep -n '<имя>' node_modules/<пакет>/types/*.d.ts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Доводы за отсрочку — это «чего это стоит»
|
|
97
|
+
|
|
98
|
+
Раздел «что стоит отложить» переезжает не в архив, а **в тела тех задач, которых он
|
|
99
|
+
касается**: там он и есть оценка цены. В файле он остаётся, только если отсрочка — решение
|
|
100
|
+
владельца, а не предложение разбора. Признак — записанный ответ владельца с датой; нет
|
|
101
|
+
его — значит, предложение.
|
|
102
|
+
|
|
103
|
+
## Ссылки на снятые разделы
|
|
104
|
+
|
|
105
|
+
Задача, чьё тело говорит `**Источник:** <документ>, раздел «…»`, после разбора ведёт в
|
|
106
|
+
пустоту, и проверка путей этого не видит: она читает файлы репозитория, а не тела задач.
|
|
107
|
+
Строка снимается тем же заходом.
|
|
108
|
+
|
|
109
|
+
## Судьба файла
|
|
110
|
+
|
|
111
|
+
- Осталось незадачное — файл живёт, и его преамбула объявляет новый признак отбора.
|
|
112
|
+
- Не осталось ничего, а документ описывал состоявшуюся работу — уезжает в `docs/archive/`.
|
|
113
|
+
- Не осталось ничего, и это был список работ — удаляется.
|
|
114
|
+
|
|
115
|
+
Разбор целиком — одна задача и одна ветка: делится то, что придётся откатывать порознь, а
|
|
116
|
+
здесь откат общий. Правка кода, найденная по дороге, в эту ветку не идёт — иначе откат разбора
|
|
117
|
+
унесёт починку.
|
|
118
|
+
|
|
119
|
+
## Что чинится в дереве следом
|
|
120
|
+
|
|
121
|
+
Документ — не единственное место, обещающее, что работа живёт в нём. Снятое имя вычищается
|
|
122
|
+
одним грепом, включая описания агентов, скилы, README и комментарии в коде:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
grep -rn 'BACKLOG' --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=archive .
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Частые промахи
|
|
129
|
+
|
|
130
|
+
- Обход по маркированным пунктам: половина работы лежит прозой и переживает разбор.
|
|
131
|
+
- «На это есть задача» без чтения её тела: пункт удалён, содержание потеряно.
|
|
132
|
+
- Число пунктов названо владельцу до сверки на выборке.
|
|
133
|
+
- Протухшее утверждение перенесено в задачу и стало действующим указанием.
|
|
134
|
+
- Доводы за отсрочку оставлены в документе: он снова становится вторым списком работ.
|
|
135
|
+
- Разбор поделён на несколько задач «по объёму» — признак деления не объём, а раздельный
|
|
136
|
+
откат.
|
|
137
|
+
- Архив тронут: он описывает состояние на момент написания и под новые термины не правится.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-style-write
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: doc-style
|
|
5
|
+
description: Паттерн правила doc-style. Брать при написании любой прозы проекта — правил, спеков, README, комментариев в коде, тел коммитов, описаний PR. Примеры «так» и «не так» на каждую договорённость о формулировке. Не брать для устройства спека — это правило spec-driven.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Как формулировать
|
|
9
|
+
|
|
10
|
+
Паттерн правила `doc-style`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/project-documentation.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Пишется правило, статья закона, пункт спека или README.
|
|
16
|
+
- Пишется комментарий в коде, тело коммита, описание PR.
|
|
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
|
+
устаревает молча. Для него заведён отдельный файл — `implementation.md` рядом.
|
|
43
|
+
|
|
44
|
+
Исключение — «Ловушки» правила: там устройство кода называется прямо, потому что ловушка и
|
|
45
|
+
есть место, где на него наступают.
|
|
46
|
+
|
|
47
|
+
## Без утверждений о будущем
|
|
48
|
+
|
|
49
|
+
«Не планируется», «не будет», «отдельная фича по запросу» — это намерение владельца, а не
|
|
50
|
+
свойство системы. Что не сделано — да, почему не сделано — да, что не будет сделано никогда —
|
|
51
|
+
нет. Вместо приговора — открытый вопрос `Q-N` с тем, что решение изменит.
|
|
52
|
+
|
|
53
|
+
Ошибка беззвучная: сверять утверждение о будущем не с чем, оно проходит любую проверку. Так в
|
|
54
|
+
первый живой спек попало «онлайн-оплаты нет и не планируется», хотя оплата в планах.
|
|
55
|
+
|
|
56
|
+
Отсюда же: того, чего нет в `docs/PRD.md`, документ о продукте не утверждает. Домысел при
|
|
57
|
+
пересказе звучит убедительнее исходника — он короче и категоричнее.
|
|
58
|
+
|
|
59
|
+
## Простыми словами
|
|
60
|
+
|
|
61
|
+
Афоризмы, метафоры и инверсии затрудняют чтение и ничего не добавляют.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
✗ запись о прошлом не должна упираться в правило, появившееся позже
|
|
65
|
+
✗ свою запись владельца стирать по молчанию площадки нельзя
|
|
66
|
+
✗ пустой список без текста и отказ без кнопки выглядят одинаково — как сломанная страница
|
|
67
|
+
|
|
68
|
+
✓ бронь, которую владелец внёс сам, пропажа события из фида не снимает
|
|
69
|
+
✓ у пустого списка должен быть текст, у отказа — кнопка повтора
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Проверка: прочитать фразу вслух. Если так не говорят — переписать.
|
|
73
|
+
|
|
74
|
+
## Факт проверяется, а не вспоминается
|
|
75
|
+
|
|
76
|
+
Перед тем как написать, что код делает X, — открыть код и посмотреть. Пересказ по памяти
|
|
77
|
+
выглядит так же уверенно, как проверенное утверждение, и отличить их потом нечем.
|
|
78
|
+
|
|
79
|
+
Особенно это касается отказов: «вернётся `value out of range`» продержалось в двух документах,
|
|
80
|
+
хотя такой отказ недостижим — в контракте и в колонке одна ширина.
|
|
81
|
+
|
|
82
|
+
## Не пересказывать то, у чего есть источник
|
|
83
|
+
|
|
84
|
+
- типы и поля контракта — `libs/common/proto/proto/<область>/v1/`, ссылкой;
|
|
85
|
+
- колонки и индексы — `prisma/schema.prisma`;
|
|
86
|
+
- числа, которые правит владелец (базовая цена, min-nights, вместимость) — только как они
|
|
87
|
+
применяются;
|
|
88
|
+
- слои и имена классов — правило `lib-layers` и сам код.
|
|
89
|
+
|
|
90
|
+
## Комментарии в коде
|
|
91
|
+
|
|
92
|
+
Те же правила. Комментарий отвечает на вопрос «почему так, а не иначе» — если ответ
|
|
93
|
+
неочевиден. Что делает строка, видно из строки.
|
|
94
|
+
|
|
95
|
+
Многострочный комментарий над каждой функцией — признак того, что объяснение подменило имя.
|
|
96
|
+
Сначала переименовать, потом писать комментарий.
|
|
97
|
+
|
|
98
|
+
Комментарий, оправдывающий отклонение от правила, держит это отклонение на себе: пока
|
|
99
|
+
объяснение выглядит убедительно, отклонение не трогают. Факт в нём проверяется, когда
|
|
100
|
+
комментарий пишут, и перепроверяется, когда отклонение снимают.
|
|
101
|
+
|
|
102
|
+
## Частые промахи
|
|
103
|
+
|
|
104
|
+
- Чужой проект назван в коммите, PR, комментарии или документе. Ни имени репозитория, ни
|
|
105
|
+
«портировано из», ни ссылок на его файлы — нигде.
|
|
106
|
+
- Число написано по памяти, а не пересчитано командой в том же коммите.
|
|
107
|
+
- Пункт плана вычеркнут по памяти о работе, а не по дереву.
|
|
108
|
+
- Формулировка звучит как заголовок, а не как правило: «работа со скидками» нарушить нельзя,
|
|
109
|
+
«применяется одна максимальная скидка» — можно.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: entity-aside
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: entity-conventions
|
|
5
|
+
description: Паттерн правила entity-conventions. Брать при сборке или правке панели создания и правки записи — готовый маршрут в аутлете ro, наследование общей основы, runMutation, гард несохранённых правок, шапка и футер, уход на связанную запись. Не брать для стора — это паттерн entity-store.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Панель правки записи
|
|
9
|
+
|
|
10
|
+
Паттерн правила `entity-conventions`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/entity-editing.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится или правится панель создания и правки записи.
|
|
16
|
+
- Появляется панель без записи — настройки таблицы, лента событий.
|
|
17
|
+
|
|
18
|
+
## Панель объявляется маршрутом в аутлете `ro`
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
{
|
|
22
|
+
path: 'booking/:id',
|
|
23
|
+
outlet: 'ro',
|
|
24
|
+
component: BookingDetailAsideComponent,
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Так панель переживает перезагрузку, передаётся ссылкой и попадает в историю браузера.
|
|
29
|
+
Программного открытия через сервис в админке нет.
|
|
30
|
+
|
|
31
|
+
Панель, которую открывают из шапки, объявляется константой и подмешивается в `children` каждой
|
|
32
|
+
доменной ветки — аутлет стоит в шаблоне хрома, а хром надевает `shell` каждого домена:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
export const ACTIVITY_ASIDE_ROUTES: Route[] = [{ path: 'activity', outlet: 'ro', component: ActivityFeedAsideComponent }];
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Экран наследует общую основу
|
|
39
|
+
|
|
40
|
+
`RtRouteAsideComponent<T>` держит `entity`, `entityId`, `isCreateMode`, `submitting`,
|
|
41
|
+
`resolving`, `submitError`, открытие панели по маршруту, уход с адреса и всю механику записи.
|
|
42
|
+
Имена из основы не переименовываются: `booking`, `feed`, `slug` вместо `entity` ломают то самое
|
|
43
|
+
единообразие, ради которого основа заведена.
|
|
44
|
+
|
|
45
|
+
## Запись идёт через `runMutation`
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
protected save(): void {
|
|
49
|
+
if (!this.canSave()) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.runMutation(this.#store.save(this.#draft()), {
|
|
54
|
+
successKey: 'promoCodeSaved',
|
|
55
|
+
errorText: (): string => this.#store.errorKey() ?? 'promoCodeSaveFailed',
|
|
56
|
+
closeOnSuccess: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Занятость, гашение прежней ошибки, тост об успехе и закрытие панели держит основа. `errorText`
|
|
62
|
+
— функция: причина отказа известна только после него. Поток мутации обязан отдать значение или
|
|
63
|
+
ошибку — пустой поток гасит панель навсегда.
|
|
64
|
+
|
|
65
|
+
## Гард несохранённых правок
|
|
66
|
+
|
|
67
|
+
Ставит сама панель, и он встаёт на все четыре пути закрытия: кнопку в шапке, кнопку в футере,
|
|
68
|
+
нажатие мимо панели и Esc.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
protected readonly panelForm: Signal<NgForm | undefined> = viewChild(NgForm);
|
|
72
|
+
protected readonly formPristine: Signal<boolean> = this.pristineSignal(
|
|
73
|
+
computed((): AbstractControl | undefined => this.panelForm()?.control)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
super();
|
|
78
|
+
this.guardUnsavedChanges({ pristine: this.formPristine, save: (): void => this.save() });
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`viewChild` на поле с `#` Angular не принимает — поле объявляется `protected`.
|
|
83
|
+
|
|
84
|
+
## Шапка и футер
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<<префикс>-aside-header [title]="title()" [overline]="overline()" [loading]="resolving()" (dismiss)="onClose()">
|
|
88
|
+
<ng-container asideActions>
|
|
89
|
+
<!-- доменные действия иконками; больше двух — под одну кнопку меню -->
|
|
90
|
+
</ng-container>
|
|
91
|
+
</<префикс>-aside-header>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Заголовок называет действие, имя записи идёт надстрочником. В футере две зоны и не больше двух
|
|
95
|
+
кнопок: `asideDismiss` — закрытие, `asidePrimary` — запись. Доменные глаголы в футер не
|
|
96
|
+
ставятся: подтверждение, отказ, снятие с публикации — иконки в шапке.
|
|
97
|
+
|
|
98
|
+
Подписи кнопок закреплены, панель их не выбирает:
|
|
99
|
+
|
|
100
|
+
| Кнопка | Подпись |
|
|
101
|
+
| ------------------------------------ | ------------------------------------------------- |
|
|
102
|
+
| запись при создании | «Создать» |
|
|
103
|
+
| запись при правке | «Сохранить» |
|
|
104
|
+
| закрытие панели, которая записывает | «Закрыть и не сохранять» (`uiCloseWithoutSaving`) |
|
|
105
|
+
| закрытие панели только для просмотра | «Закрыть» |
|
|
106
|
+
|
|
107
|
+
Кнопка записи стоит у противоположного края от кнопки закрытия, а пока идёт запрос —
|
|
108
|
+
показывает спиннер и не нажимается: занятость берётся из `submitting()` основы, своего флага
|
|
109
|
+
панель не заводит. Шапка и футер при прокрутке остаются на месте — прокручивается только зона
|
|
110
|
+
содержимого.
|
|
111
|
+
|
|
112
|
+
## Уход на связанную запись
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<a rtElem="related" qa-dataid="promo-code-property-link" [href]="propertyHref()" (click)="openProperty($event)">
|
|
116
|
+
{{ 'propertyOpenLink' | transloco }} <<префикс>-icon name="arrow-right" size="sm" />
|
|
117
|
+
</a>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Адрес даёт `relatedUrl(commands)`, уход — `openRelated(commands)`. `routerLink` здесь не
|
|
121
|
+
годится: директива навигирует сама, `preventDefault` её не останавливает, и вопрос о
|
|
122
|
+
несохранённых правках она обходит.
|
|
123
|
+
|
|
124
|
+
## Частые промахи
|
|
125
|
+
|
|
126
|
+
- Свой `router.navigate` в панели: абсолютные команды меняют только первичную ветку, аутлет
|
|
127
|
+
остаётся в адресе, и роутер отклоняет навигацию молча.
|
|
128
|
+
- Скелетоны по `busy()`, а не по `resolving()`: `busy` включает и запись, и на сохранении поля
|
|
129
|
+
превратились бы в скелетоны.
|
|
130
|
+
- Своё «не найдено» на панели: ненайденная запись уводит с адреса силами основы.
|
|
131
|
+
- Свои отступы поверх общей основы: они дают разную ширину полей на разных панелях и срезают
|
|
132
|
+
обводку фокуса у края прокрутки.
|
|
133
|
+
- Панель, остающаяся открытой после успеха, не сбросила нетронутость (`markAsPristine`) —
|
|
134
|
+
вопрос о правках задаётся сразу после записи.
|
|
135
|
+
- Отключённые поля ввода вместо данных: запись, которую только смотрят, показывается через
|
|
136
|
+
`<dl>`, `<префикс>-detail-list`, `<префикс>-info-item`.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: entity-models-new
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: entity-models
|
|
5
|
+
description: Паттерн правила entity-models. Брать при объявлении новой модели сущности и её маппера — готовый неймспейс I<Сущность> с Api, State и Draft, короткий и полный уровни, наследник BaseMapper с typeCast, что делать после правки .proto.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Объявить модель сущности и её перевод
|
|
9
|
+
|
|
10
|
+
Паттерн правила `entity-models`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/entity-models.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится новая сущность админки.
|
|
16
|
+
- У существующей появляется короткий уровень.
|
|
17
|
+
- Правится маппер или контракт этой сущности.
|
|
18
|
+
|
|
19
|
+
## Модель — неймспейс в `util` домена
|
|
20
|
+
|
|
21
|
+
Файл `libs/<семья>/<домен>/util/src/lib/models/<сущность>.model.ts`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
export namespace IPromoCode {
|
|
25
|
+
export namespace Short {
|
|
26
|
+
export type Api = PromoCodeListItem;
|
|
27
|
+
|
|
28
|
+
export interface State {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly code: string;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Api = PromoCodeInfo;
|
|
35
|
+
|
|
36
|
+
export interface State extends Short.State {
|
|
37
|
+
readonly usageCount: number;
|
|
38
|
+
/** Пусто = код действует на любой объект владельца */
|
|
39
|
+
readonly propertyId: string;
|
|
40
|
+
/** 0 = без предела */
|
|
41
|
+
readonly usageLimit: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Что уходит на сервер при сохранении: id пуст — код новый */
|
|
45
|
+
export interface Draft {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly code: string;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export enum EPromoDiscountKind {
|
|
52
|
+
Percent = 'percent',
|
|
53
|
+
Amount = 'amount',
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Перечисления домена лежат в том же файле, но **вне** неймспейса. Глубже двух уровней
|
|
58
|
+
вложенности не заводить: `IPromoCode.Short.State` читается, третий уровень уже нет.
|
|
59
|
+
|
|
60
|
+
Псевдонимы выборки объявляются там же:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
export type Query = IList.Query.State<EPromoCodeSortProperty, EPromoCodeFilterProperty>;
|
|
64
|
+
export type ListResult = IList.Result.State<IPromoCode.State, EPromoCodeSortProperty, EPromoCodeFilterProperty>;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Маппер — в `api` домена, свой на каждый уровень
|
|
68
|
+
|
|
69
|
+
Файл `libs/<семья>/<домен>/api/src/lib/mappers/<сущность>-model.mapper.ts`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
export class PromoCodeModelMapper extends BaseMapper<IPromoCode.State> {
|
|
73
|
+
public override mapFrom(raw: IPromoCode.Api): IPromoCode.State {
|
|
74
|
+
return {
|
|
75
|
+
id: this.typeCast.getAsString(raw?.id),
|
|
76
|
+
code: this.typeCast.getAsString(raw?.code),
|
|
77
|
+
usageCount: this.typeCast.getAsNumber(raw?.usageCount),
|
|
78
|
+
usageLimit: this.typeCast.getAsNumber(raw?.usageLimit),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Маппер без состояния и без DI:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
readonly #mapper: PromoCodeModelMapper = new PromoCodeModelMapper();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
На каждый уровень — свой класс: `PromoCodeShortModelMapper` и `PromoCodeModelMapper`.
|
|
91
|
+
|
|
92
|
+
## Строковое поле с конечным набором сверяется явно
|
|
93
|
+
|
|
94
|
+
`getAsType` умолчания не принимает: значение вне набора он пишет в консоль и возвращает
|
|
95
|
+
строкой `'unknown'`.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
kind: promoDiscountKindOf(raw?.kind) ?? EPromoDiscountKind.Percent,
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## После правки контракта
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd libs/common/proto && npx buf lint
|
|
105
|
+
npm run proto:generate
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Ни `buf lint`, ни `buf breaking` не входят в `check:all` и в CI — гоняются руками.
|
|
109
|
+
Сгенерированные типы лежат в репозитории, и без перегенерации расхождение вылезет сборкой
|
|
110
|
+
чужого приложения.
|
|
111
|
+
|
|
112
|
+
Снятое поле помечается `reserved` с его номером и именем.
|
|
113
|
+
|
|
114
|
+
## Частые промахи
|
|
115
|
+
|
|
116
|
+
- `Api` переписан руками вместо псевдонима — разойдётся с контрактом молча.
|
|
117
|
+
- `??` вместо `typeCast` — контракт отдаёт значения по умолчанию, и проверка на `undefined`
|
|
118
|
+
не ловит ничего.
|
|
119
|
+
- `as Type` в маппере — запрещено правилом `typescript-conventions`.
|
|
120
|
+
- `null` или `undefined` в `State` — пустое выражается пустой строкой или нулём, а смысл нуля
|
|
121
|
+
объясняется комментарием рядом с полем.
|
|
122
|
+
- `readonly`-массив отдан в запрос: init-тип сообщения требует изменяемый, модель отдаётся
|
|
123
|
+
копией.
|
|
124
|
+
- Общий тип на админку и сайт — у гостя своя короткая форма записи.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: entity-store
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: entity-conventions
|
|
5
|
+
description: Паттерн правила entity-conventions. Брать при заведении или правке стора админки — готовый наследник общей основы списочного стора, обвязка mutate, имена методов от действия, действие со своей занятостью. Не брать для панели — это паттерн entity-aside.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Стор сущности
|
|
9
|
+
|
|
10
|
+
Паттерн правила `entity-conventions`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/entity-editing.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится `<сущность>.store.ts`.
|
|
16
|
+
- Правится метод загрузки или записи существующего стора.
|
|
17
|
+
|
|
18
|
+
## Наследник объявляет своё в четырёх строках
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class PromoCodesStore extends BaseListStoreService<
|
|
23
|
+
IPromoCodesState,
|
|
24
|
+
string,
|
|
25
|
+
IPromoCode.State,
|
|
26
|
+
EPromoCodeSortProperty,
|
|
27
|
+
EPromoCodeFilterProperty,
|
|
28
|
+
IPromoCode.Draft
|
|
29
|
+
> {
|
|
30
|
+
protected override readonly apiService: PromoCodeApiService = inject(PromoCodeApiService);
|
|
31
|
+
protected override readonly listErrorKey: string = 'promoCodesLoadFailed';
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
super({ ...INITIAL_STATE.LIST }, { name: 'PromoCodesStore' });
|
|
35
|
+
|
|
36
|
+
this.setConfig({ usePagination: true, useSorting: true, useFiltering: true, useSearch: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected override mutationErrorKeyOf(error: unknown): string {
|
|
40
|
+
return promoRejectionKey(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Конфиг решает, что уходит в выборку. Выключено всё — выборки нет вовсе, и сервер отдаёт список
|
|
46
|
+
целиком. Из основы работают загрузка и перезапрос, смена страницы, порядка, условий отбора и
|
|
47
|
+
строки поиска, догрузка следующей страницы, правка одной записи в списке и сброс выборки.
|
|
48
|
+
|
|
49
|
+
Файл называется `<сущность>.store.ts`: имя `<сущность>-store.service.ts` выводит его и из
|
|
50
|
+
правила линтера, и из гейта скилов.
|
|
51
|
+
|
|
52
|
+
## Метод правки отдаёт поток
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
public save(draft: IPromoCode.Draft): Observable<IPromoCode.State | null> {
|
|
56
|
+
return this.mutate(this.apiService.save(draft));
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`mutate` держит занятость, гашение прежней ошибки, перечитывание списка после успеха и ключ
|
|
61
|
+
отказа. Мутация завершается **перечитанным списком**, а не отправленным запросом: панель
|
|
62
|
+
закрывается по значению потока, и список, перечитанный после закрытия, показал бы прежнее
|
|
63
|
+
значение.
|
|
64
|
+
|
|
65
|
+
## Имена — от действия, а не от домена
|
|
66
|
+
|
|
67
|
+
| ✗ | ✓ |
|
|
68
|
+
| ------------------------------------ | ---------- |
|
|
69
|
+
| `createBooking()`, `updateBooking()` | `save()` |
|
|
70
|
+
| `deleteBooking()`, `removeFeed()` | `remove()` |
|
|
71
|
+
| `loadBookings()`, `fetchFeeds()` | `load()` |
|
|
72
|
+
|
|
73
|
+
Имя домена уже в имени стора и в его алиасе.
|
|
74
|
+
|
|
75
|
+
## Действие со своей занятостью
|
|
76
|
+
|
|
77
|
+
Идёт мимо `mutate`: опрос подписки на календарь держит `pollingId`, потому что панель на минуту
|
|
78
|
+
опроса не гасится, а ключ отказа кладёт `setErrorKey`.
|
|
79
|
+
|
|
80
|
+
## Частые промахи
|
|
81
|
+
|
|
82
|
+
- Булев ответ у метода правки: он теряет и записанную запись, и причину отказа — панель узнаёт
|
|
83
|
+
только «не вышло».
|
|
84
|
+
- Своя обвязка занятости и ошибки вокруг вызова сервиса: всё это в `mutate`.
|
|
85
|
+
- Свои сигналы записей, занятости и отказа: они в общей основе.
|
|
86
|
+
- Подписка на сигнал отказа загрузки: сигнал делят список и панель, и один отказ показался бы
|
|
87
|
+
дважды. Отказ загрузки идёт отдельным потоком.
|
|
88
|
+
- Второе поле сортировки: порядок в выборке один — его не принимают ни таблица, ни контракт,
|
|
89
|
+
ни разбор на сервере.
|
|
90
|
+
- Хвост с `EMPTY`, приклеенный к мутации: он гасится `defaultIfEmpty`, иначе отказ приклеенного
|
|
91
|
+
потока превращает удачную запись в вечный спиннер.
|