@rt-tools/agent-kit 0.5.3 → 0.7.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 (85) hide show
  1. package/README.md +15 -1
  2. package/assets/checks/check-file-size.mjs +127 -0
  3. package/assets/checks/check-push-gate.mjs +139 -0
  4. package/assets/checks/rt-kit-checks.config.mjs +21 -0
  5. package/assets/defaults/gate-map.sh +90 -34
  6. package/assets/defaults/project.sh +26 -3
  7. package/assets/hooks/skill-gate-layers.sh +156 -0
  8. package/assets/hooks/skill-gate.sh +11 -2
  9. package/assets/laws/code-structure.md +3 -0
  10. package/assets/laws/delivery.md +9 -0
  11. package/assets/laws/observability.md +46 -0
  12. package/assets/laws/project-documentation.md +4 -0
  13. package/assets/laws/reuse-first.md +2 -0
  14. package/assets/laws/verifiability.md +5 -0
  15. package/assets/patterns/browser-verification-stand.md +22 -2
  16. package/assets/patterns/doc-style-trace.md +111 -0
  17. package/assets/patterns/git-workflow-commit.azure.md +18 -0
  18. package/assets/patterns/git-workflow-commit.github.md +19 -1
  19. package/assets/patterns/git-workflow-commit.gitlab.md +18 -0
  20. package/assets/patterns/git-workflow-docker.md +203 -0
  21. package/assets/patterns/git-workflow-secrets.md +93 -0
  22. package/assets/patterns/observability-record.md +114 -0
  23. package/assets/patterns/seo-verify.md +1 -1
  24. package/assets/patterns/spec-driven-domain.md +2 -2
  25. package/assets/patterns/spec-driven-rule.md +5 -0
  26. package/assets/patterns/styling-bem-sheet.md +178 -0
  27. package/assets/patterns/task-flow-close.md +20 -0
  28. package/assets/patterns/task-flow-resume.md +5 -0
  29. package/assets/patterns/translations-content.md +107 -0
  30. package/assets/patterns/translations-key.md +1 -1
  31. package/assets/rules/angular-patterns.md +5 -0
  32. package/assets/rules/browser-verification.md +17 -12
  33. package/assets/rules/component-structure.md +6 -2
  34. package/assets/rules/doc-style.md +16 -0
  35. package/assets/rules/git-workflow.azure.md +60 -1
  36. package/assets/rules/git-workflow.github.md +67 -1
  37. package/assets/rules/git-workflow.gitlab.md +61 -1
  38. package/assets/rules/lists.md +13 -0
  39. package/assets/rules/observability.md +147 -0
  40. package/assets/rules/permissions.md +23 -0
  41. package/assets/rules/reuse-first.md +9 -0
  42. package/assets/rules/seo.md +57 -9
  43. package/assets/rules/shared-code.md +6 -0
  44. package/assets/rules/spec-driven.md +9 -0
  45. package/assets/rules/styling-bem.md +34 -1
  46. package/assets/rules/task-flow.md +5 -0
  47. package/assets/rules/testing.md +46 -8
  48. package/assets/rules/translations.md +11 -5
  49. package/assets/rules/typescript-conventions.md +5 -0
  50. package/lib/assets.d.ts +1 -1
  51. package/lib/assets.d.ts.map +1 -1
  52. package/lib/assets.js +2 -4
  53. package/lib/assets.js.map +1 -1
  54. package/lib/catalog.d.ts +73 -0
  55. package/lib/catalog.d.ts.map +1 -1
  56. package/lib/catalog.js +121 -0
  57. package/lib/catalog.js.map +1 -1
  58. package/lib/commands.d.ts.map +1 -1
  59. package/lib/commands.js +84 -5
  60. package/lib/commands.js.map +1 -1
  61. package/lib/integrity.d.ts +25 -12
  62. package/lib/integrity.d.ts.map +1 -1
  63. package/lib/integrity.js +40 -18
  64. package/lib/integrity.js.map +1 -1
  65. package/lib/proposals.d.ts +9 -1
  66. package/lib/proposals.d.ts.map +1 -1
  67. package/lib/proposals.js +11 -2
  68. package/lib/proposals.js.map +1 -1
  69. package/lib/retired.d.ts +30 -0
  70. package/lib/retired.d.ts.map +1 -0
  71. package/lib/retired.js +19 -0
  72. package/lib/retired.js.map +1 -0
  73. package/lib/sync.d.ts +45 -1
  74. package/lib/sync.d.ts.map +1 -1
  75. package/lib/sync.js +44 -10
  76. package/lib/sync.js.map +1 -1
  77. package/package.json +1 -1
  78. package/rt-tools-agent-kit-0.7.0.tgz +0 -0
  79. package/assets/laws/application/money.md +0 -41
  80. package/assets/laws/application/ownership.md +0 -32
  81. package/assets/patterns/ownership-scope-resolve.md +0 -69
  82. package/assets/patterns/pricing-quote.md +0 -71
  83. package/assets/rules/ownership-scope.md +0 -63
  84. package/assets/rules/pricing.md +0 -64
  85. package/rt-tools-agent-kit-0.5.3.tgz +0 -0
@@ -2,7 +2,7 @@
2
2
  name: testing
3
3
  kind: rule
4
4
  law: verifiability
5
- description: Правило под «Закон о проверяемости». Брать при правке любого *.spec.ts и всего, что лежит в apps/site-e2e и apps/admin-e2e. Называет Vitest и Playwright, идентификатор сценария в заголовке, вынос решения в чистую функцию и то, что закрывается сквозной спекой. Готовый код — в паттернах testing-unit и testing-e2e.
5
+ description: Правило под «Закон о проверяемости». Брать при правке любого *.spec.ts и всего, что лежит в сквозных наборах дерева. Называет Vitest и Playwright, идентификатор сценария в заголовке, вынос решения в чистую функцию и то, что закрывается сквозной спекой. Готовый код — в паттернах testing-unit и testing-e2e.
6
6
  ---
7
7
 
8
8
  # Проверяемость — как это устроено здесь
@@ -45,8 +45,21 @@ description: Правило под «Закон о проверяемости».
45
45
  переименованный или выкинутый сценарий: тесты при этом остаются зелёными.
46
46
  - **Решение выносится в чистую функцию и проверяется вызовом.** Компонент и сервис остаются
47
47
  тонкой обёрткой и отдельно не проверяются, пока своего ветвления у них нет.
48
+ - **Решение, зависящее от текущего момента, принимает момент параметром.** Часы машины оно не
49
+ читает: правило проверяется вызовом, а не подкруткой времени вокруг теста. Умолчание
50
+ `= new Date()` ставится на границе — там, где решение зовёт процедура или служба.
48
51
  - **Процедура Connect проверяется вызовом своего метода с рукописным двойником базы.**
49
52
  Контейнер и роутер поднимать не надо: спека проверяет решение, а не раскладку полей.
53
+ - **Сид заводит то, без чего экран не открыть, и ничего, что гость примет за настоящее.**
54
+ Содержимое, у которого есть автор, — отзывы, вопросы, обсуждения — гость читает как
55
+ написанное людьми, а стенд собирается из той же базы, что и проверка глазами: три выдуманные
56
+ цитаты дожили до отдельной задачи и всё это время выглядели отзывами. Настройки владельца —
57
+ контакты, адрес отправителя, ключи внешних служб — сид не заводит тоже: их вписывают в
58
+ приложении. То, что нужно редко, включается признаком в окружении, а не сеется всем.
59
+ - **Заведённая проверка встаёт в гейт пуша или в конвейер, а не только в сводную цель.**
60
+ Сводная цель запускается руками, и проверка, которая живёт только в ней, отвечает тому, кто
61
+ её вспомнил: молчание такой проверки читается как её зелёный ответ. Три проверки простояли
62
+ вне гейта, объявляя в собственных списках известного, что падают на новом.
50
63
  - **Спека, необратимо меняющая данные стенда, выключена по умолчанию.** `BASE_URL` уводит
51
64
  прогон одной переменной, и без выключателя такая спека правила бы данные чужого стенда.
52
65
  - **Спеки, которым нужен nginx перед приложением, просыпаются вместе с `BASE_URL`.** Голый
@@ -56,6 +69,24 @@ description: Правило под «Закон о проверяемости».
56
69
  `playwright/no-skipped-test` запретил бы его сразу в шестидесяти пяти местах. Рядом со
57
70
  строкой отключения пишут причину, а точечный `eslint-disable` остаётся для того, что
58
71
  запрещено по делу.
72
+ - **Гард отпускает действие, когда сам сломался.** Нет разборщика входа, пустой ввод, не тот
73
+ каталог, любая своя ошибка — гард выходит нулём и пропускает: сломанная проверка не имеет
74
+ права заклинить работу. Объявляется это строкой `FAIL-OPEN` в шапке самого гарда, рядом с
75
+ перечислением случаев, и туда же дописывается новый случай, когда он находится.
76
+ - **Список известного у проверки именной и объясняет себя сам.** Первым полем перечня стоит имя
77
+ проверки и слово о том, что перечисленное отказом не считается. Дальше либо два ключа —
78
+ принятое остаётся навсегда, долг накоплен к заведению проверки и только сокращается, — либо
79
+ столько ключей, сколько у записей родов. Причина обязательна: снятая проверка без причины
80
+ через месяц неотличима от недосмотра.
81
+ - **Тест, утверждающий отсутствие, зелен и тогда, когда ищет не то.** Совпадения нет ни у
82
+ верного текста, ни у опечатки в образце, ни у переименованного ключа — отличить их по цвету
83
+ прогона нечем. Отрицательное утверждение поэтому идёт в паре с положительным: сначала
84
+ проверяется, что искомое место вообще найдено, и только потом — что в нём нет того, чего быть
85
+ не должно.
86
+ - **Заголовок теста обещает больше, чем тело проверяет, и сверка этого не видит.** Номер
87
+ сценария в заголовке стоит — сценарий числится покрытым, а что именно утверждается, не
88
+ спрашивает никто. Тело читается вместе с заголовком: обещание в заголовке и утверждение в
89
+ теле — два разных текста, и расходятся они молча.
59
90
 
60
91
  ## Чего из закона здесь нет
61
92
 
@@ -76,23 +107,30 @@ description: Правило под «Закон о проверяемости».
76
107
  - **Зелёный `nx test <проект>` не значит, что хоть один файл исполнялся.** Либа без своего
77
108
  `vitest.config.mts` не запускает ничего — так тесты домена броней не запускались ни разу.
78
109
  Либа с конфигом, но без единого `*.spec.ts`, проходит зелёной из-за `passWithNoTests: true`,
79
- который стоит во всех 203 конфигах дерева, и на глаз эти два случая неотличимы: в обоих
80
- прогон успешен. Без единого теста живут 131 либа из 203 почти две трети. Перед правкой в
110
+ который обычно стоит в каждом конфиге дерева, и на глаз эти два случая неотличимы: в обоих
111
+ прогон успешен. Доля либ без единого теста меряется пересчётом нижев дереве, где его
112
+ завели впервые, она вышла почти в две трети. Перед правкой в
81
113
  незнакомой либе проверяется, есть ли в ней хоть один `*.spec.ts`; если нет — первый
82
114
  заводится этой же правкой, а не откладывается: откладывать здесь не с чего, долг уже
83
115
  накоплен. Пересчёт: `for d in $(find libs -name vitest.config.mts -exec dirname {} \;); do
84
116
  [ -z "$(find "$d" -name '*.spec.ts')" ] && echo "$d"; done | wc -l`.
117
+ - **Зелёная сводка покрытия не значит, что тесты проходят.** Сверка читает заголовки тестов и
118
+ сопоставляет их со сценариями спека; исполняется ли тест и чем он кончается — она не знает
119
+ вовсе, и падающий тест значится в ней покрытием. Три сценария одной панели падали и до правки
120
+ экрана, а нашлось это только прогоном. Перед правкой экрана его сквозные тесты гоняются один
121
+ раз до первой строки кода: иначе чужое падение читается как своя регрессия, а своё — как
122
+ чужое.
85
123
  - **«Executable doesn't exist» — состояние машины, а не дефект правки.** Установлен только
86
124
  chromium, `firefox` и `webkit` падают всегда: гонять `--project=chromium`, узкий экран —
87
125
  `--project=mobile-chrome`. Та же ошибка приходит после смены версии Playwright: браузер
88
126
  ставится под конкретную версию, и после подъёма нужен повторный
89
127
  `npx playwright install chromium`. Девять тестов так и упали, и это выглядело регрессией
90
128
  обновления.
91
- - **Первому прогону сразу после установки браузера верить нельзя.** Два падения `admin-e2e`
92
- не повторились ни при отдельном прогоне тех же тестов, ни при втором полном. Такой прогон
93
- повторяют, а выводы делают по второму.
94
- - Сквозные тесты админки без `E2E_ADMIN_EMAIL` и `E2E_ADMIN_PASSWORD` пропускаются молча — в
95
- отчёте они значатся `skipped`, и прогон выглядит успешным.
129
+ - **Первому прогону сразу после установки браузера верить нельзя.** Два падения сквозного
130
+ набора не повторились ни при отдельном прогоне тех же тестов, ни при втором полном. Такой
131
+ прогон повторяют, а выводы делают по второму.
132
+ - Сквозная спека, которой нужен вход, без учётных данных в окружении пропускается молча — в
133
+ отчёте она значится `skipped`, и прогон выглядит успешным. Имена переменных — при дереве.
96
134
  - **Справочник флоу вторых сценариев не заводит.** В `docs/E2E_<ДОМЕН>_FLOWS.md` кладут то,
97
135
  чего в спеке домена нет и быть не должно: `qa-dataid` элементов, состояния разметки, ловушки
98
136
  стенда. Обещанное поведение остаётся сценарием в `scenarios.md`: если списать его во второе
@@ -2,7 +2,7 @@
2
2
  name: translations
3
3
  kind: rule
4
4
  law: locales
5
- description: Правило под «Закон о локалях и переводах». Брать при заведении любого видимого текста, правке словарей libs/common/i18n, префиксов локалей сайта и перевода контента объекта. Называет восемь локалей, Transloco, производные переводы и начальную валюту локали. Готовый код — в паттерне translations-key.
5
+ description: Правило под «Закон о локалях и переводах». Брать при заведении любого видимого текста, правке словарей libs/common/i18n, префиксов локалей сайта и перевода контента объекта. Называет локали перевода, словари, производные переводы и начальную валюту локали. Готовый код — в паттерне translations-key.
6
6
  ---
7
7
 
8
8
  # Локали и переводы — как это устроено здесь
@@ -15,7 +15,7 @@ description: Правило под «Закон о локалях и перев
15
15
 
16
16
  | В законе | Здесь |
17
17
  | -------------------- | --------------------------------------------------------------------------------------------------- |
18
- | локаль сайта | одна из восьми: `en`, `ru`, `de`, `zh-Hans`, `zh-Hant`, `ko`, `th`, `hi` |
18
+ | локаль сайта | одна из локалей перевода; их набор в `implementation.md` рядом |
19
19
  | локаль по умолчанию | `en` — отдаётся из корня, без префикса в адресе |
20
20
  | локаль ввода | `source_locale` в запросе сохранения объекта; берётся из языка админки |
21
21
  | словарь | JSON-словари Transloco в `libs/common/i18n/src/lib/dictionaries/<локаль>/`, разложенные по разделам |
@@ -54,6 +54,8 @@ description: Правило под «Закон о локалях и перев
54
54
  ## Паттерны
55
55
 
56
56
  - `translations-key` — заведение ключа во всех локалях перевода и подстановка в разметку.
57
+ - `translations-content` — перевод содержимого записи: производный перевод, запись локали
58
+ руками, отказ провайдера, сброс кэша отданных страниц.
57
59
 
58
60
  ## Ловушки
59
61
 
@@ -63,7 +65,11 @@ description: Правило под «Закон о локалях и перев
63
65
  на общий, сайт, письма и админку.
64
66
  - **Перевод контента идёт до транзакции сохранения:** страница объекта не должна оказаться
65
67
  наполовину переведённой. Кэш сбрасывается после записи и один раз.
66
- - **Без ключа `ANTHROPIC_API_KEY` сохранение проходит,** но переводы остаются прежними, и
67
- владелец видит предупреждение `propertySaveTranslationFailed`.
68
+ - **Без ключа переводов сохранение проходит,** но переводы остаются прежними, и владелец видит
69
+ предупреждение об этом на своём экране. Ключ лежит секретом владения в хранилище, а не
70
+ переменной окружения, и заводит его владелец экраном интеграций: одноимённая переменная в
71
+ составе прода приложением не читается. Состояния ключа — паттерн `git-workflow-secrets`.
72
+ - **Ветки локалей надеты не на все маршруты сайта, и новый раздел в них не попадает** —
73
+ раскладка, её ловушка и готовый код лежат в правиле `seo` и паттерне `seo-page`.
68
74
  - Новый маршрут сайта без ветки под каждую локаль существует только в локали по умолчанию:
69
- `/de/<путь>` отдаст 404 и поисковику, и гостю.
75
+ путь под префиксом другой локали отдаст 404 и поисковику, и гостю.
@@ -33,6 +33,11 @@ description: Правило под «Закон об устройстве код
33
33
  различаются в месте использования, а не переходом к объявлению.
34
34
  - **Суффикс имени файла находит в нём обещанное объявление.** Список суффиксов закрыт: слово,
35
35
  которого в нём нет, суффиксом не считается, и такой файл правило не судит.
36
+ - **Файл не длиннее 500 строк, и считаются все строки — пустые и комментарии тоже.** Файл,
37
+ который не влезает на экран целиком, читают по частям, и правку в нём делают, не увидев
38
+ остального. Для `.ts` это держит правило линтера; файлы обвязки — сценарии и скрипты — до
39
+ него не доходят и судятся отдельной проверкой дерева. Накопленное к дню включения
40
+ перечислено поимённо, и строка оттуда снимается вместе с делением своего файла.
36
41
  - **Тип берётся из того пакета, где объявлен.** Своя копия чужого типа расходится с оригиналом
37
42
  молча, а компилируется из них только одна.
38
43
  - **Двухступенчатое приведение `as unknown as` запрещено правилом линтера.** Вместо него —
package/lib/assets.d.ts CHANGED
@@ -19,6 +19,6 @@ export interface IAsset {
19
19
  * ресурса нет. У разложенного `git-workflow` хостинга в имени не остаётся: второго вида в этом
20
20
  * дереве нет, а агенту, который правило читает, различать их незачем.
21
21
  */
22
- export declare function targetOf(entry: IEntryOfCatalog, layout: Readonly<Record<TKind, string>>): string;
22
+ export declare function targetOf(entry: Pick<IEntryOfCatalog, 'id' | 'kind' | 'name'>, layout: Readonly<Record<TKind, string>>): string;
23
23
  export declare function collectAssets(config: IConfig, assetsDir: string): readonly IAsset[];
24
24
  //# sourceMappingURL=assets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/assets.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,eAAe,EAAyB,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,OAAO,EAA2B,KAAK,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,WAAW,MAAM;IACnB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,8FAA8F;IAC9F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAChC;AAUD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAIhG;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAWnF"}
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/assets.ts"],"names":[],"mappings":"AASA,OAAO,EAAiB,eAAe,EAAe,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,OAAO,EAA2B,KAAK,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,WAAW,MAAM;IACnB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,8FAA8F;IAC9F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAChC;AAUD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAI9H;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CASnF"}
package/lib/assets.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * показывает и то, от чего проект отказался, — обоим нужен набор до отбора.
7
7
  */
8
8
  import { join } from 'node:path';
9
- import { isChosen, readCatalog } from './catalog.js';
9
+ import { chosenEntries, readCatalog } from './catalog.js';
10
10
  import { SKILL_FILE, SKILL_KINDS } from './config.js';
11
11
  /** Расширение ресурса вместе с точкой; у файла без расширения — пустая строка. */
12
12
  const extensionOf = (id) => {
@@ -27,9 +27,7 @@ export function targetOf(entry, layout) {
27
27
  : join(layout[entry.kind], `${entry.name}${extensionOf(entry.id)}`);
28
28
  }
29
29
  export function collectAssets(config, assetsDir) {
30
- return readCatalog(assetsDir)
31
- .filter((entry) => isChosen(entry, config))
32
- .map((entry) => ({
30
+ return chosenEntries(readCatalog(assetsDir), config).map((entry) => ({
33
31
  id: entry.id,
34
32
  kind: entry.kind,
35
33
  name: entry.name,
package/lib/assets.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/assets.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAmB,QAAQ,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAW,UAAU,EAAE,WAAW,EAAS,MAAM,aAAa,CAAC;AAetE,kFAAkF;AAClF,MAAM,WAAW,GAA2B,CAAC,EAAU,EAAU,EAAE;IAC/D,MAAM,GAAG,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,KAAK,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAE1C,OAAO,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC5C,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAsB,EAAE,MAAuC;IACpF,OAAO,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QACnC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC;QAClD,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,SAAiB;IAC5D,OAAO,WAAW,CAAC,SAAS,CAAC;SACxB,MAAM,CAAC,CAAC,KAAsB,EAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;SACpE,GAAG,CAAC,CAAC,KAAsB,EAAU,EAAE,CAAC,CAAC;QACtC,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC;QACtC,UAAU,EAAE,KAAK,CAAC,UAAU;KAC/B,CAAC,CAAC,CAAC;AACZ,CAAC","sourcesContent":["/**\n * Что из везомого пакетом взял этот проект и куда оно ложится.\n *\n * Полный набор читает `catalog.ts`, здесь из него остаётся выбранное. Разделено потому, что\n * выбирать приходится раньше, чем раскладывать: `init` спрашивает, какие законы взять, а `list`\n * показывает и то, от чего проект отказался, — обоим нужен набор до отбора.\n */\nimport { join } from 'node:path';\n\nimport { IEntryOfCatalog, isChosen, readCatalog } from './catalog.js';\nimport { IConfig, SKILL_FILE, SKILL_KINDS, TKind } from './config.js';\n\nexport interface IAsset {\n /** Идентификатор: `laws/application/money.md`. Он же путь надстройки и ключ в `only` и `skip`. */\n readonly id: string;\n readonly kind: TKind;\n /** Имя без рода, вида и расширения: `delivery`. У скила — имя каталога, которым его зовут. */\n readonly name: string;\n readonly text: string;\n /** Путь в дереве проекта, куда ресурс ложится. */\n readonly target: string;\n /** Запускается ли файл сам по себе: гарда без права на запуск не существует. */\n readonly executable: boolean;\n}\n\n/** Расширение ресурса вместе с точкой; у файла без расширения — пустая строка. */\nconst extensionOf: (id: string) => string = (id: string): string => {\n const dot: number = id.lastIndexOf('.');\n const slash: number = id.lastIndexOf('/');\n\n return dot > slash ? id.slice(dot) : '';\n};\n\n/**\n * Куда ложится ресурс. Скил читается агентом как `<имя>/SKILL.md`, и другого имени у него быть\n * не может: файл, названный по ресурсу, агент не найдёт вовсе. Остальные роды ложатся файлом —\n * по имени, а не по идентификатору: слой законов из идентификатора в путь переезжает, а вид\n * ресурса нет. У разложенного `git-workflow` хостинга в имени не остаётся: второго вида в этом\n * дереве нет, а агенту, который правило читает, различать их незачем.\n */\nexport function targetOf(entry: IEntryOfCatalog, layout: Readonly<Record<TKind, string>>): string {\n return SKILL_KINDS.includes(entry.kind)\n ? join(layout[entry.kind], entry.name, SKILL_FILE)\n : join(layout[entry.kind], `${entry.name}${extensionOf(entry.id)}`);\n}\n\nexport function collectAssets(config: IConfig, assetsDir: string): readonly IAsset[] {\n return readCatalog(assetsDir)\n .filter((entry: IEntryOfCatalog): boolean => isChosen(entry, config))\n .map((entry: IEntryOfCatalog): IAsset => ({\n id: entry.id,\n kind: entry.kind,\n name: entry.name,\n text: entry.text,\n target: targetOf(entry, config.layout),\n executable: entry.executable,\n }));\n}\n"]}
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/assets.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAmB,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAW,UAAU,EAAE,WAAW,EAAS,MAAM,aAAa,CAAC;AAetE,kFAAkF;AAClF,MAAM,WAAW,GAA2B,CAAC,EAAU,EAAU,EAAE;IAC/D,MAAM,GAAG,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,KAAK,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAE1C,OAAO,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC5C,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAoD,EAAE,MAAuC;IAClH,OAAO,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QACnC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC;QAClD,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,SAAiB;IAC5D,OAAO,aAAa,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAsB,EAAU,EAAE,CAAC,CAAC;QAC1F,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC;QACtC,UAAU,EAAE,KAAK,CAAC,UAAU;KAC/B,CAAC,CAAC,CAAC;AACR,CAAC","sourcesContent":["/**\n * Что из везомого пакетом взял этот проект и куда оно ложится.\n *\n * Полный набор читает `catalog.ts`, здесь из него остаётся выбранное. Разделено потому, что\n * выбирать приходится раньше, чем раскладывать: `init` спрашивает, какие законы взять, а `list`\n * показывает и то, от чего проект отказался, — обоим нужен набор до отбора.\n */\nimport { join } from 'node:path';\n\nimport { chosenEntries, IEntryOfCatalog, readCatalog } from './catalog.js';\nimport { IConfig, SKILL_FILE, SKILL_KINDS, TKind } from './config.js';\n\nexport interface IAsset {\n /** Идентификатор: `laws/application/money.md`. Он же путь надстройки и ключ в `only` и `skip`. */\n readonly id: string;\n readonly kind: TKind;\n /** Имя без рода, вида и расширения: `delivery`. У скила — имя каталога, которым его зовут. */\n readonly name: string;\n readonly text: string;\n /** Путь в дереве проекта, куда ресурс ложится. */\n readonly target: string;\n /** Запускается ли файл сам по себе: гарда без права на запуск не существует. */\n readonly executable: boolean;\n}\n\n/** Расширение ресурса вместе с точкой; у файла без расширения — пустая строка. */\nconst extensionOf: (id: string) => string = (id: string): string => {\n const dot: number = id.lastIndexOf('.');\n const slash: number = id.lastIndexOf('/');\n\n return dot > slash ? id.slice(dot) : '';\n};\n\n/**\n * Куда ложится ресурс. Скил читается агентом как `<имя>/SKILL.md`, и другого имени у него быть\n * не может: файл, названный по ресурсу, агент не найдёт вовсе. Остальные роды ложатся файлом —\n * по имени, а не по идентификатору: слой законов из идентификатора в путь переезжает, а вид\n * ресурса нет. У разложенного `git-workflow` хостинга в имени не остаётся: второго вида в этом\n * дереве нет, а агенту, который правило читает, различать их незачем.\n */\nexport function targetOf(entry: Pick<IEntryOfCatalog, 'id' | 'kind' | 'name'>, layout: Readonly<Record<TKind, string>>): string {\n return SKILL_KINDS.includes(entry.kind)\n ? join(layout[entry.kind], entry.name, SKILL_FILE)\n : join(layout[entry.kind], `${entry.name}${extensionOf(entry.id)}`);\n}\n\nexport function collectAssets(config: IConfig, assetsDir: string): readonly IAsset[] {\n return chosenEntries(readCatalog(assetsDir), config).map((entry: IEntryOfCatalog): IAsset => ({\n id: entry.id,\n kind: entry.kind,\n name: entry.name,\n text: entry.text,\n target: targetOf(entry, config.layout),\n executable: entry.executable,\n }));\n}\n"]}
package/lib/catalog.d.ts CHANGED
@@ -30,6 +30,22 @@ export interface IEntryOfCatalog {
30
30
  */
31
31
  readonly requires: readonly string[];
32
32
  }
33
+ /** Что объявила шапка ресурса. Пустое поле и отсутствующее здесь одно и то же. */
34
+ export interface IFrontMatter {
35
+ readonly name: string;
36
+ readonly kind: string;
37
+ /** Закон, под которым стоит правило. */
38
+ readonly law: string;
39
+ /** Правило, при котором стоит паттерн. */
40
+ readonly rule: string;
41
+ }
42
+ /**
43
+ * Вступление между `---` в начале файла; его нет — вернётся пустая шапка.
44
+ *
45
+ * Живёт при каталоге, потому что связь родителя с потомком читают двое: сверка связности пакета
46
+ * и каскад отказа. Объявление у одного из них заводило бы круговой импорт между ними.
47
+ */
48
+ export declare function frontMatterOf(text: string): IFrontMatter;
33
49
  /**
34
50
  * Что ресурс о себе объявил. Имена читаются идентификаторами — `hooks/observe.sh`: короткая
35
51
  * форма совпала бы у закона и правила с одним именем, а требование, указавшее не туда, хуже
@@ -91,6 +107,63 @@ export interface IGapOfVariant {
91
107
  * существующие в `skip` — дерево тем самым говорит, что обходится без этого ресурса.
92
108
  */
93
109
  export declare function variantGaps(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IGapOfVariant[];
110
+ /** Ресурс, снятый вслед за отвергнутым родителем. */
111
+ export interface ICascadeCut {
112
+ readonly id: string;
113
+ /** Ближайший родитель: у правила — его закон, у паттерна — его правило. */
114
+ readonly parent: string;
115
+ /**
116
+ * Отвергнутый корень цепочки — то, что дерево действительно отвергло или не выбрало.
117
+ * Ближайшего родителя мало: паттерн уходит вслед за правилом, которого в отказе нет, и
118
+ * искать его там читатель пойдёт зря.
119
+ */
120
+ readonly root: string;
121
+ }
122
+ /**
123
+ * Ресурсы, снятые каскадом: правила при невзятом законе и паттерны при невзятых правилах.
124
+ *
125
+ * Связь читается из вступления самого ресурса — отдельный список при пакете разошёлся бы с
126
+ * ресурсами молча, тем же молчанием, ради которого каскад и заводится. Родитель ищется по
127
+ * последнему звену имени: полного пути в шапке нет, потому что переезд закона между слоями
128
+ * переписывал бы шапки всех правил при нём.
129
+ *
130
+ * Родителя в каталоге нет вовсе — каскад молчит: снимать не по чему, а промах в шапке судит
131
+ * сверка связности пакета, до всякой раскладки в дереве.
132
+ */
133
+ export declare function cascadeCuts(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly ICascadeCut[];
134
+ /**
135
+ * Ресурсы, названные выбором поимённо и всё равно снятые каскадом.
136
+ *
137
+ * Дерево, назвавшее правило в выборе при невыбранном законе, не получает его молча — то самое
138
+ * молчание, ради которого каскад и заводится, только с другой стороны: там о снятии не знал тот,
139
+ * кто ничего не просил, здесь — тот, кто попросил прямо.
140
+ */
141
+ export declare function namedButCut(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly ICascadeCut[];
142
+ /**
143
+ * Что ложится в дерево: прямой отбор за вычетом снятого каскадом.
144
+ *
145
+ * Отдельной функцией, а не внутри `isChosen`, потому что каскад знает весь каталог, а отбор —
146
+ * один ресурс: связь родителя с потомком по одной записи не видна.
147
+ */
148
+ export declare function chosenEntries(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IEntryOfCatalog[];
149
+ /** Строка отказа, которая ничего не снимает. */
150
+ export interface IIdleSkip {
151
+ readonly id: string;
152
+ /** Чем названное ею уже снято: имя отвергнутого родителя, либо пусто — такого ресурса нет. */
153
+ readonly by: string;
154
+ }
155
+ /**
156
+ * Строки отказа, которые ничего не снимают: названное ими снято каскадом или в каталоге не
157
+ * значится вовсе.
158
+ *
159
+ * Не отказ, а предупреждение: настройка, верная до обновления пакета, становится лишней сама,
160
+ * без единой правки в дереве, и отбивать за это раскладку значило бы наказывать за вчерашнюю
161
+ * правоту.
162
+ *
163
+ * Строка на ресурс чужого вида лишней не считается никогда: ею дерево гасит отказ о ресурсе,
164
+ * у которого нет подходящего вида, — то есть работу она делает.
165
+ */
166
+ export declare function idleSkips(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IIdleSkip[];
94
167
  /** Выбранный ресурс, чьё требование в дерево не поехало. */
95
168
  export interface IBrokenLink {
96
169
  /** Кто требует — идентификатор выбранного ресурса. */
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/catalog.ts"],"names":[],"mappings":"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"}
package/lib/catalog.js CHANGED
@@ -10,6 +10,29 @@ import { join } from 'node:path';
10
10
  import { KINDS } from './config.js';
11
11
  import { matchesVariant, readAxes, variantOf, withoutVariant } from './variants.js';
12
12
  const TITLE = /^#\s+(\S.*)$/m;
13
+ const FIELD = /^([a-z]+):\s*(\S.*?)\s*$/;
14
+ /**
15
+ * Вступление между `---` в начале файла; его нет — вернётся пустая шапка.
16
+ *
17
+ * Живёт при каталоге, потому что связь родителя с потомком читают двое: сверка связности пакета
18
+ * и каскад отказа. Объявление у одного из них заводило бы круговой импорт между ними.
19
+ */
20
+ export function frontMatterOf(text) {
21
+ const lines = text.split('\n');
22
+ const found = {};
23
+ if (lines[0]?.trim() === '---') {
24
+ for (const line of lines.slice(1)) {
25
+ if (line.trim() === '---') {
26
+ break;
27
+ }
28
+ const match = line.match(FIELD);
29
+ if (match) {
30
+ found[match[1]] = match[2];
31
+ }
32
+ }
33
+ }
34
+ return { name: found['name'] ?? '', kind: found['kind'] ?? '', law: found['law'] ?? '', rule: found['rule'] ?? '' };
35
+ }
13
36
  /**
14
37
  * Строка требования в шапке ресурса. Двух видов, потому что ресурсы двух родов: у разметки
15
38
  * `**Требует:**`, у исполняемого файла — комментарий `# Требует:`.
@@ -174,6 +197,104 @@ export function variantGaps(catalog, selection) {
174
197
  }
175
198
  return gaps;
176
199
  }
200
+ /** Последнее звено имени: `application/money` → `money`. Им ресурсы и ссылаются друг на друга. */
201
+ const shortNameOf = (entry) => entry.name.split('/').pop() ?? entry.name;
202
+ /**
203
+ * Ресурсы, снятые каскадом: правила при невзятом законе и паттерны при невзятых правилах.
204
+ *
205
+ * Связь читается из вступления самого ресурса — отдельный список при пакете разошёлся бы с
206
+ * ресурсами молча, тем же молчанием, ради которого каскад и заводится. Родитель ищется по
207
+ * последнему звену имени: полного пути в шапке нет, потому что переезд закона между слоями
208
+ * переписывал бы шапки всех правил при нём.
209
+ *
210
+ * Родителя в каталоге нет вовсе — каскад молчит: снимать не по чему, а промах в шапке судит
211
+ * сверка связности пакета, до всякой раскладки в дереве.
212
+ */
213
+ export function cascadeCuts(catalog, selection) {
214
+ const takenOf = (kind) => new Set(catalog.filter((entry) => entry.kind === kind && isChosen(entry, selection)).map(shortNameOf));
215
+ const knownOf = (kind) => new Set(catalog.filter((entry) => entry.kind === kind).map(shortNameOf));
216
+ const takenLaws = takenOf('laws');
217
+ const knownLaws = knownOf('laws');
218
+ const takenRules = takenOf('rules');
219
+ const knownRules = knownOf('rules');
220
+ const cuts = [];
221
+ /** Правило, снятое законом: для паттернов при нём корнем цепочки будет этот закон. */
222
+ const rootByRule = new Map();
223
+ for (const entry of catalog) {
224
+ if (entry.kind !== 'rules' || !isChosen(entry, selection)) {
225
+ continue;
226
+ }
227
+ const law = frontMatterOf(entry.text).law;
228
+ if (!law || !knownLaws.has(law) || takenLaws.has(law)) {
229
+ continue;
230
+ }
231
+ cuts.push({ id: entry.id, parent: law, root: law });
232
+ rootByRule.set(shortNameOf(entry), law);
233
+ }
234
+ for (const entry of catalog) {
235
+ if (entry.kind !== 'patterns' || !isChosen(entry, selection)) {
236
+ continue;
237
+ }
238
+ const rule = frontMatterOf(entry.text).rule;
239
+ if (!rule || !knownRules.has(rule) || (takenRules.has(rule) && !rootByRule.has(rule))) {
240
+ continue;
241
+ }
242
+ cuts.push({ id: entry.id, parent: rule, root: rootByRule.get(rule) ?? rule });
243
+ }
244
+ return cuts;
245
+ }
246
+ /**
247
+ * Ресурсы, названные выбором поимённо и всё равно снятые каскадом.
248
+ *
249
+ * Дерево, назвавшее правило в выборе при невыбранном законе, не получает его молча — то самое
250
+ * молчание, ради которого каскад и заводится, только с другой стороны: там о снятии не знал тот,
251
+ * кто ничего не просил, здесь — тот, кто попросил прямо.
252
+ */
253
+ export function namedButCut(catalog, selection) {
254
+ return cascadeCuts(catalog, selection).filter((one) => selection.only.includes(one.id));
255
+ }
256
+ /**
257
+ * Что ложится в дерево: прямой отбор за вычетом снятого каскадом.
258
+ *
259
+ * Отдельной функцией, а не внутри `isChosen`, потому что каскад знает весь каталог, а отбор —
260
+ * один ресурс: связь родителя с потомком по одной записи не видна.
261
+ */
262
+ export function chosenEntries(catalog, selection) {
263
+ const cut = new Set(cascadeCuts(catalog, selection).map((one) => one.id));
264
+ return catalog.filter((entry) => isChosen(entry, selection) && !cut.has(entry.id));
265
+ }
266
+ /**
267
+ * Строки отказа, которые ничего не снимают: названное ими снято каскадом или в каталоге не
268
+ * значится вовсе.
269
+ *
270
+ * Не отказ, а предупреждение: настройка, верная до обновления пакета, становится лишней сама,
271
+ * без единой правки в дереве, и отбивать за это раскладку значило бы наказывать за вчерашнюю
272
+ * правоту.
273
+ *
274
+ * Строка на ресурс чужого вида лишней не считается никогда: ею дерево гасит отказ о ресурсе,
275
+ * у которого нет подходящего вида, — то есть работу она делает.
276
+ */
277
+ export function idleSkips(catalog, selection) {
278
+ const idle = [];
279
+ for (const id of selection.skip) {
280
+ const entry = catalog.find((one) => one.id === id);
281
+ if (!entry) {
282
+ idle.push({ id, by: '' });
283
+ continue;
284
+ }
285
+ if (!matchesVariant(entry.variant, selection.variants)) {
286
+ continue;
287
+ }
288
+ // Каскад считается от отбора без этой строки: иначе снятое ею самой читалось бы как
289
+ // снятое родителем, и лишней оказалась бы любая строка отказа подряд.
290
+ const without = { ...selection, skip: selection.skip.filter((one) => one !== id) };
291
+ const cut = cascadeCuts(catalog, without).find((one) => one.id === id);
292
+ if (cut) {
293
+ idle.push({ id, by: cut.root });
294
+ }
295
+ }
296
+ return idle;
297
+ }
177
298
  /**
178
299
  * Связи, разорванные выбором дерева.
179
300
  *