@rt-tools/agent-kit 0.3.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.
Files changed (201) hide show
  1. package/README.md +194 -30
  2. package/assets/agents/business-analyst.md +74 -0
  3. package/assets/agents/project-manager.md +70 -0
  4. package/assets/agents/qa-engineer.md +72 -0
  5. package/assets/agents/skill-curator.md +110 -0
  6. package/assets/agents/spec-critic.md +44 -0
  7. package/assets/agents/spec-writer.md +50 -0
  8. package/assets/checks/board.github.mjs +286 -0
  9. package/assets/checks/check-board.github.mjs +188 -0
  10. package/assets/checks/check-doc-paths.mjs +163 -0
  11. package/assets/checks/check-dupes.mjs +277 -0
  12. package/assets/checks/check-lib-layers.mjs +573 -0
  13. package/assets/checks/check-reuse.mjs +208 -0
  14. package/assets/checks/check-schema-drift.mjs +186 -0
  15. package/assets/checks/check-specs.mjs +1007 -0
  16. package/assets/checks/check-styles.mjs +109 -0
  17. package/assets/checks/rt-kit-checks.config.mjs +134 -0
  18. package/assets/checks/task-new.github.mjs +198 -0
  19. package/assets/commands/skill-curator.md +70 -0
  20. package/assets/defaults/gate-map.sh +100 -0
  21. package/assets/defaults/project.sh +179 -0
  22. package/assets/hooks/browser-device-id.sh +0 -0
  23. package/assets/hooks/browser-guard-device-id.sh +2 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +2 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
  27. package/assets/hooks/browser-guard-require-select.sh +2 -1
  28. package/assets/hooks/commit-msg.sh +1 -1
  29. package/assets/hooks/constitution-index.sh +5 -4
  30. package/assets/hooks/dev-server-guard.sh +8 -6
  31. package/assets/hooks/docs-guard.sh +223 -37
  32. package/assets/hooks/git-guard-delivery.sh +86 -29
  33. package/assets/hooks/git-guard-main.sh +1 -0
  34. package/assets/hooks/git-guard-push-tests.sh +34 -13
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +155 -30
  37. package/assets/hooks/qa-dataid-guard.sh +72 -32
  38. package/assets/hooks/reuse-first-guard.sh +105 -34
  39. package/assets/hooks/skill-gate-rearm.sh +1 -0
  40. package/assets/hooks/skill-gate.sh +75 -15
  41. package/assets/hooks/skill-loaded.sh +1 -0
  42. package/assets/hooks/sql-guard.sh +606 -56
  43. package/assets/hooks/task-context-load.sh +100 -0
  44. package/assets/hooks/task-flow-guard.sh +107 -0
  45. package/assets/laws/{access.md → application/access.md} +1 -4
  46. package/assets/laws/{locales.md → application/locales.md} +1 -3
  47. package/assets/laws/application/money.md +41 -0
  48. package/assets/laws/application/ownership.md +32 -0
  49. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  50. package/assets/laws/code-structure.md +7 -6
  51. package/assets/laws/delivery.md +53 -3
  52. package/assets/laws/entity-editing.md +49 -55
  53. package/assets/laws/entity-models.md +4 -14
  54. package/assets/laws/frontend-application.md +5 -5
  55. package/assets/laws/lib-imports.md +14 -1
  56. package/assets/laws/lists.md +33 -0
  57. package/assets/laws/navigation.md +40 -0
  58. package/assets/laws/project-documentation.md +17 -8
  59. package/assets/laws/reuse-first.md +26 -21
  60. package/assets/laws/shared-code.md +13 -1
  61. package/assets/laws/verifiability.md +17 -1
  62. package/assets/laws/work-conduct.md +48 -0
  63. package/assets/patterns/admin-lists-screen.md +131 -0
  64. package/assets/patterns/admin-nav-item.md +71 -0
  65. package/assets/patterns/angular-patterns-state.md +29 -22
  66. package/assets/patterns/api-layer-pair.md +40 -30
  67. package/assets/patterns/browser-verification-measure.md +41 -38
  68. package/assets/patterns/browser-verification-stand.md +106 -42
  69. package/assets/patterns/component-structure-new.md +33 -32
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +65 -28
  72. package/assets/patterns/doc-style-write.md +36 -33
  73. package/assets/patterns/entity-aside.md +136 -0
  74. package/assets/patterns/entity-models-new.md +124 -0
  75. package/assets/patterns/entity-store.md +91 -0
  76. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  77. package/assets/patterns/git-workflow-commit.github.md +333 -0
  78. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  79. package/assets/patterns/git-workflow-merge.md +42 -25
  80. package/assets/patterns/git-workflow-migration.md +61 -31
  81. package/assets/patterns/git-workflow-restart.md +20 -20
  82. package/assets/patterns/lib-layers-move.md +50 -32
  83. package/assets/patterns/lib-layers-new.md +41 -29
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +35 -33
  86. package/assets/patterns/platform-access-di.md +39 -25
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +22 -22
  89. package/assets/patterns/seo-page.md +52 -40
  90. package/assets/patterns/seo-verify.md +48 -29
  91. package/assets/patterns/shared-code-new.md +37 -31
  92. package/assets/patterns/spec-driven-domain.md +44 -37
  93. package/assets/patterns/spec-driven-rule.md +55 -40
  94. package/assets/patterns/styling-bem-component.md +43 -32
  95. package/assets/patterns/styling-bem-layout.md +30 -24
  96. package/assets/patterns/task-flow-close.md +90 -0
  97. package/assets/patterns/task-flow-resume.md +94 -0
  98. package/assets/patterns/task-flow-start.md +117 -0
  99. package/assets/patterns/testing-e2e.md +53 -51
  100. package/assets/patterns/testing-unit.md +70 -46
  101. package/assets/patterns/translations-key.md +32 -19
  102. package/assets/patterns/ts-procedure.md +24 -25
  103. package/assets/rules/angular-patterns.md +46 -27
  104. package/assets/rules/api-layer.md +46 -28
  105. package/assets/rules/browser-verification.md +66 -48
  106. package/assets/rules/component-structure.md +43 -27
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +81 -39
  109. package/assets/rules/entity-conventions.md +78 -0
  110. package/assets/rules/entity-models.md +70 -0
  111. package/assets/rules/git-workflow.azure.md +116 -0
  112. package/assets/rules/git-workflow.github.md +123 -0
  113. package/assets/rules/git-workflow.gitlab.md +113 -0
  114. package/assets/rules/lib-layers.md +56 -30
  115. package/assets/rules/lists.md +73 -0
  116. package/assets/rules/navigation.md +78 -0
  117. package/assets/rules/ownership-scope.md +63 -0
  118. package/assets/rules/permissions.md +43 -25
  119. package/assets/rules/platform-access.md +57 -29
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +57 -43
  122. package/assets/rules/seo.md +51 -30
  123. package/assets/rules/shared-code.md +51 -26
  124. package/assets/rules/spec-driven.md +96 -50
  125. package/assets/rules/styling-bem.md +54 -39
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +78 -47
  128. package/assets/rules/translations.md +48 -31
  129. package/assets/rules/typescript-conventions.md +57 -27
  130. package/assets/skills/agent-kit.md +81 -0
  131. package/assets/skills/write-a-skill.md +108 -0
  132. package/assets/templates/gate-map.sh +23 -15
  133. package/assets/templates/implementation.md +14 -8
  134. package/assets/templates/pattern.md +1 -1
  135. package/assets/templates/project.sh +32 -19
  136. package/assets/templates/rule.md +1 -1
  137. package/assets/variants.json +20 -0
  138. package/assets/workflows/feature.js +134 -0
  139. package/assets/workflows/plan.js +150 -0
  140. package/bin/agent-kit.d.ts.map +1 -1
  141. package/bin/agent-kit.js +78 -5
  142. package/bin/agent-kit.js.map +1 -1
  143. package/bin/prompt.d.ts +5 -0
  144. package/bin/prompt.d.ts.map +1 -1
  145. package/bin/prompt.js +19 -7
  146. package/bin/prompt.js.map +1 -1
  147. package/index.d.ts +1 -0
  148. package/index.d.ts.map +1 -1
  149. package/index.js +1 -0
  150. package/index.js.map +1 -1
  151. package/lib/assets.d.ts +8 -3
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +13 -3
  154. package/lib/assets.js.map +1 -1
  155. package/lib/catalog.d.ts +52 -5
  156. package/lib/catalog.d.ts.map +1 -1
  157. package/lib/catalog.js +104 -16
  158. package/lib/catalog.js.map +1 -1
  159. package/lib/commands.d.ts +22 -1
  160. package/lib/commands.d.ts.map +1 -1
  161. package/lib/commands.js +202 -14
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +5 -1
  164. package/lib/companion.d.ts.map +1 -1
  165. package/lib/companion.js +29 -2
  166. package/lib/companion.js.map +1 -1
  167. package/lib/config.d.ts +26 -9
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +41 -15
  170. package/lib/config.js.map +1 -1
  171. package/lib/freshness.d.ts +14 -0
  172. package/lib/freshness.d.ts.map +1 -0
  173. package/lib/freshness.js +116 -0
  174. package/lib/freshness.js.map +1 -0
  175. package/lib/hooks-map.d.ts +24 -0
  176. package/lib/hooks-map.d.ts.map +1 -0
  177. package/lib/hooks-map.js +72 -0
  178. package/lib/hooks-map.js.map +1 -0
  179. package/lib/integrity.d.ts +36 -0
  180. package/lib/integrity.d.ts.map +1 -0
  181. package/lib/integrity.js +44 -0
  182. package/lib/integrity.js.map +1 -0
  183. package/lib/picker.d.ts +11 -1
  184. package/lib/picker.d.ts.map +1 -1
  185. package/lib/picker.js +44 -6
  186. package/lib/picker.js.map +1 -1
  187. package/lib/sync.d.ts +26 -0
  188. package/lib/sync.d.ts.map +1 -1
  189. package/lib/sync.js +59 -4
  190. package/lib/sync.js.map +1 -1
  191. package/lib/variants.d.ts +44 -0
  192. package/lib/variants.d.ts.map +1 -0
  193. package/lib/variants.js +82 -0
  194. package/lib/variants.js.map +1 -0
  195. package/package.json +1 -1
  196. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  197. package/assets/laws/admin-lists.md +0 -35
  198. package/assets/laws/admin-navigation.md +0 -38
  199. package/assets/patterns/git-workflow-commit.md +0 -175
  200. package/assets/rules/git-workflow.md +0 -106
  201. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
package/lib/sync.js CHANGED
@@ -5,14 +5,18 @@
5
5
  * только потом писать. `sync --check` отличается от `sync` ровно последним шагом — иначе гейт
6
6
  * пуша проверял бы не то, что кладёт раскладка.
7
7
  */
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { dirname, join } from 'node:path';
10
- import { collectAssets } from './assets.js';
10
+ import { collectAssets, targetOf } from './assets.js';
11
+ import { readCatalog, variantGaps } from './catalog.js';
11
12
  import { pathOf, planCompanion } from './companion.js';
12
13
  import { OVERRIDES_DIR } from './config.js';
14
+ import { bindingOf, unboundHooks } from './hooks-map.js';
13
15
  import { isPending, isRefusal, planFile } from './plan.js';
14
16
  import { mergeDocuments, parseDocument, renderDocument } from './sections.js';
17
+ import { readStamped } from './stamp.js';
15
18
  import { renderVars } from './vars.js';
19
+ import { matchesVariant } from './variants.js';
16
20
  /** Шаблон черновика компаньона — тот же, что пакет кладёт проекту в шаблоны. */
17
21
  const COMPANION_TEMPLATE = 'templates/implementation.md';
18
22
  const read = (path) => (existsSync(path) ? readFileSync(path, 'utf8') : null);
@@ -27,6 +31,40 @@ function renderAsset(asset, config, root) {
27
31
  const merged = override ? renderDocument(mergeDocuments(parseDocument(asset.text), parseDocument(override))) : asset.text;
28
32
  return renderVars(merged, config.vars);
29
33
  }
34
+ /**
35
+ * Что лежит в дереве от ресурсов, которые проект больше не берёт.
36
+ *
37
+ * Ищется по тем же правилам раскладки, что и всё остальное: путь ресурса вычисляется так, будто
38
+ * его кладут, и проверяется, лежит ли там файл с шапкой пакета. Чужой файл на том же пути
39
+ * брошенным не считается — его пакет не клал.
40
+ */
41
+ function abandonedOf(config, root, assetsDir) {
42
+ const taken = new Set(collectAssets(config, assetsDir).map((asset) => asset.id));
43
+ return readCatalog(assetsDir)
44
+ .filter((entry) => !taken.has(entry.id) && matchesVariant(entry.variant, config.variants))
45
+ .map((entry) => targetOf(entry, config.layout))
46
+ .filter((path) => {
47
+ const existing = read(join(root, path));
48
+ return existing !== null && readStamped(existing) !== null;
49
+ });
50
+ }
51
+ /**
52
+ * Что о себе говорят гарды, которые дерево берёт. Читается у ресурсов, а не у разложенных
53
+ * файлов: карта нужна и до первой раскладки — чтобы было что вставить в настройку.
54
+ */
55
+ function bindingsOf(config, assetsDir) {
56
+ const bindings = [];
57
+ for (const asset of collectAssets(config, assetsDir)) {
58
+ if (asset.kind !== 'hooks') {
59
+ continue;
60
+ }
61
+ const binding = bindingOf(asset.text, asset.target);
62
+ if (binding) {
63
+ bindings.push(binding);
64
+ }
65
+ }
66
+ return bindings;
67
+ }
30
68
  export function planSync(config, root, version, assetsDir) {
31
69
  const planned = [];
32
70
  const missing = new Map();
@@ -43,14 +81,28 @@ export function planSync(config, root, version, assetsDir) {
43
81
  companions.push(planCompanion(asset, read(join(root, pathOf(asset))), template));
44
82
  }
45
83
  }
46
- return { planned, missing, companions, written: [] };
84
+ return {
85
+ planned,
86
+ missing,
87
+ companions,
88
+ abandoned: abandonedOf(config, root, assetsDir),
89
+ gaps: variantGaps(readCatalog(assetsDir), config),
90
+ unbound: unboundHooks(bindingsOf(config, assetsDir), root),
91
+ written: [],
92
+ };
47
93
  }
48
94
  /** Раскладка. Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого. */
49
95
  export function runSync(config, root, version, assetsDir) {
50
96
  const result = planSync(config, root, version, assetsDir);
51
- if (result.missing.size || result.planned.some((entry) => isRefusal(entry.outcome))) {
97
+ if (result.missing.size || result.gaps.length || result.planned.some((entry) => isRefusal(entry.outcome))) {
52
98
  return result;
53
99
  }
100
+ // Право на запуск переносится с файла в пакете. Без него гард отвечает отказом доступа, то
101
+ // есть ненулевым кодом, а ненулевой код у гарда значит «правка отбита»: разложенный набор
102
+ // отбивал бы подряд всё, включая сборку и тесты, и причина при этом нигде не называлась.
103
+ const executable = new Set(collectAssets(config, assetsDir)
104
+ .filter((asset) => asset.executable)
105
+ .map((asset) => asset.id));
54
106
  const written = [];
55
107
  for (const entry of result.planned) {
56
108
  if (entry.content === null) {
@@ -59,6 +111,9 @@ export function runSync(config, root, version, assetsDir) {
59
111
  const path = join(root, entry.path);
60
112
  mkdirSync(dirname(path), { recursive: true });
61
113
  writeFileSync(path, entry.content, 'utf8');
114
+ if (executable.has(entry.asset)) {
115
+ chmodSync(path, 0o755);
116
+ }
62
117
  written.push(entry.path);
63
118
  }
64
119
  // Черновик компаньона кладётся только там, где файла нет вовсе. Он без шапки и без суммы:
package/lib/sync.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Раскладка ресурсов пакета в дерево проекта.\n *\n * Порядок один и тот же и для записи, и для проверки: собрать тело, решить судьбу файла, и\n * только потом писать. `sync --check` отличается от `sync` ровно последним шагом — иначе гейт\n * пуша проверял бы не то, что кладёт раскладка.\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { collectAssets, IAsset } from './assets.js';\nimport { ICompanion, pathOf, planCompanion } from './companion.js';\nimport { IConfig, OVERRIDES_DIR } from './config.js';\nimport { IPlanned, isPending, isRefusal, planFile } from './plan.js';\nimport { mergeDocuments, parseDocument, renderDocument } from './sections.js';\nimport { IRenderResult, renderVars } from './vars.js';\n\nexport interface ISyncResult {\n readonly planned: readonly IPlanned[];\n /** Дырки без значения, по ресурсам. Непустой список — отказ: подставлять нечего. */\n readonly missing: ReadonlyMap<string, readonly string[]>;\n /** Компаньоны разложенных правил: имена этого дерева, которые пишет проект. */\n readonly companions: readonly ICompanion[];\n readonly written: readonly string[];\n}\n\n/** Шаблон черновика компаньона — тот же, что пакет кладёт проекту в шаблоны. */\nconst COMPANION_TEMPLATE: string = 'templates/implementation.md';\n\nconst read: (path: string) => string | null = (path: string): string | null => (existsSync(path) ? readFileSync(path, 'utf8') : null);\n\n/**\n * Тело ресурса: текст пакета, поверх него надстройка проекта, и уже потом подстановка значений.\n *\n * Порядок именно такой. Надстройка тоже пишется с дырками — иначе проект, дописавший раздел про\n * свою главную ветку, зашил бы её имя в двух местах: в конфиге и в тексте.\n */\nfunction renderAsset(asset: IAsset, config: IConfig, root: string): IRenderResult {\n const override: string | null = read(join(root, OVERRIDES_DIR, asset.id));\n const merged: string = override ? renderDocument(mergeDocuments(parseDocument(asset.text), parseDocument(override))) : asset.text;\n\n return renderVars(merged, config.vars);\n}\n\nexport function planSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const planned: IPlanned[] = [];\n const missing: Map<string, readonly string[]> = new Map();\n const companions: ICompanion[] = [];\n const template: string | null = read(join(assetsDir, COMPANION_TEMPLATE));\n\n for (const asset of collectAssets(config, assetsDir)) {\n const rendered: IRenderResult = renderAsset(asset, config, root);\n if (rendered.missing.length) {\n missing.set(asset.id, rendered.missing);\n continue;\n }\n planned.push(\n planFile({ path: asset.target, asset: asset.id, version, rendered: rendered.text, existing: read(join(root, asset.target)) })\n );\n if (asset.kind === 'rules' && template !== null) {\n companions.push(planCompanion(asset, read(join(root, pathOf(asset))), template));\n }\n }\n\n return { planned, missing, companions, written: [] };\n}\n\n/** Раскладка. Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого. */\nexport function runSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n if (result.missing.size || result.planned.some((entry: IPlanned): boolean => isRefusal(entry.outcome))) {\n return result;\n }\n\n const written: string[] = [];\n for (const entry of result.planned) {\n if (entry.content === null) {\n continue;\n }\n const path: string = join(root, entry.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, entry.content, 'utf8');\n written.push(entry.path);\n }\n\n // Черновик компаньона кладётся только там, где файла нет вовсе. Он без шапки и без суммы:\n // сверять в нём нечего — с первой правки проекта это его текст, а не текст пакета.\n for (const companion of result.companions) {\n if (companion.content === null) {\n continue;\n }\n const path: string = join(root, companion.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, companion.content, 'utf8');\n written.push(companion.path);\n }\n\n return { ...result, written };\n}\n\nexport const pendingOf: (result: ISyncResult) => readonly IPlanned[] = (result: ISyncResult): readonly IPlanned[] =>\n result.planned.filter((entry: IPlanned): boolean => isPending(entry.outcome));\n"]}
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Раскладка ресурсов пакета в дерево проекта.\n *\n * Порядок один и тот же и для записи, и для проверки: собрать тело, решить судьбу файла, и\n * только потом писать. `sync --check` отличается от `sync` ровно последним шагом — иначе гейт\n * пуша проверял бы не то, что кладёт раскладка.\n */\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { collectAssets, IAsset, targetOf } from './assets.js';\nimport { IEntryOfCatalog, IGapOfVariant, readCatalog, variantGaps } from './catalog.js';\nimport { ICompanion, pathOf, planCompanion } from './companion.js';\nimport { IConfig, OVERRIDES_DIR } from './config.js';\nimport { bindingOf, IHookBinding, unboundHooks } from './hooks-map.js';\nimport { IPlanned, isPending, isRefusal, planFile } from './plan.js';\nimport { mergeDocuments, parseDocument, renderDocument } from './sections.js';\nimport { readStamped } from './stamp.js';\nimport { IRenderResult, renderVars } from './vars.js';\nimport { matchesVariant } from './variants.js';\n\nexport interface ISyncResult {\n readonly planned: readonly IPlanned[];\n /** Дырки без значения, по ресурсам. Непустой список — отказ: подставлять нечего. */\n readonly missing: ReadonlyMap<string, readonly string[]>;\n /** Компаньоны разложенных правил: имена этого дерева, которые пишет проект. */\n readonly companions: readonly ICompanion[];\n /**\n * Файлы, лежащие в дереве от ресурсов, от которых проект отказался.\n *\n * Отказ значит «больше не клади», а не «убери»: разложенное коммитится, рядом с правилом\n * лежит написанный проектом компаньон, и стирать это молча пакет не вправе. Но и молчать\n * нельзя — брошенный файл читается как действующее правило, и агент по нему работает.\n */\n readonly abandoned: readonly string[];\n /**\n * Ресурсы, у которых виды есть, а вида под выбор дерева нет.\n *\n * Пропустить такой ресурс молча значит разложить правило и не разложить инструмент, который\n * оно зовёт: дерево получает указание без исполнителя и узнаёт об этом, когда команда из\n * правила не находится. Поэтому непустой список — отказ раскладки наравне с дыркой без\n * значения и с файлом, который правили руками.\n */\n readonly gaps: readonly IGapOfVariant[];\n /**\n * Разложенные гарды, которых нет в настройке агента: файл лежит, а позвать его некому.\n *\n * Раскладку это не отбивает — настройка принадлежит дереву, и пакет в неё не пишет, — но и\n * молчать нельзя: гард, который не зовут, неотличим от гарда, который всё пропускает.\n */\n readonly unbound: readonly IHookBinding[];\n readonly written: readonly string[];\n}\n\n/** Шаблон черновика компаньона — тот же, что пакет кладёт проекту в шаблоны. */\nconst COMPANION_TEMPLATE: string = 'templates/implementation.md';\n\nconst read: (path: string) => string | null = (path: string): string | null => (existsSync(path) ? readFileSync(path, 'utf8') : null);\n\n/**\n * Тело ресурса: текст пакета, поверх него надстройка проекта, и уже потом подстановка значений.\n *\n * Порядок именно такой. Надстройка тоже пишется с дырками — иначе проект, дописавший раздел про\n * свою главную ветку, зашил бы её имя в двух местах: в конфиге и в тексте.\n */\nfunction renderAsset(asset: IAsset, config: IConfig, root: string): IRenderResult {\n const override: string | null = read(join(root, OVERRIDES_DIR, asset.id));\n const merged: string = override ? renderDocument(mergeDocuments(parseDocument(asset.text), parseDocument(override))) : asset.text;\n\n return renderVars(merged, config.vars);\n}\n\n/**\n * Что лежит в дереве от ресурсов, которые проект больше не берёт.\n *\n * Ищется по тем же правилам раскладки, что и всё остальное: путь ресурса вычисляется так, будто\n * его кладут, и проверяется, лежит ли там файл с шапкой пакета. Чужой файл на том же пути\n * брошенным не считается — его пакет не клал.\n */\nfunction abandonedOf(config: IConfig, root: string, assetsDir: string): readonly string[] {\n const taken: ReadonlySet<string> = new Set(collectAssets(config, assetsDir).map((asset: IAsset): string => asset.id));\n\n return readCatalog(assetsDir)\n .filter((entry: IEntryOfCatalog): boolean => !taken.has(entry.id) && matchesVariant(entry.variant, config.variants))\n .map((entry: IEntryOfCatalog): string => targetOf(entry, config.layout))\n .filter((path: string): boolean => {\n const existing: string | null = read(join(root, path));\n\n return existing !== null && readStamped(existing) !== null;\n });\n}\n\n/**\n * Что о себе говорят гарды, которые дерево берёт. Читается у ресурсов, а не у разложенных\n * файлов: карта нужна и до первой раскладки — чтобы было что вставить в настройку.\n */\nfunction bindingsOf(config: IConfig, assetsDir: string): readonly IHookBinding[] {\n const bindings: IHookBinding[] = [];\n for (const asset of collectAssets(config, assetsDir)) {\n if (asset.kind !== 'hooks') {\n continue;\n }\n const binding: IHookBinding | null = bindingOf(asset.text, asset.target);\n if (binding) {\n bindings.push(binding);\n }\n }\n\n return bindings;\n}\n\nexport function planSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const planned: IPlanned[] = [];\n const missing: Map<string, readonly string[]> = new Map();\n const companions: ICompanion[] = [];\n const template: string | null = read(join(assetsDir, COMPANION_TEMPLATE));\n\n for (const asset of collectAssets(config, assetsDir)) {\n const rendered: IRenderResult = renderAsset(asset, config, root);\n if (rendered.missing.length) {\n missing.set(asset.id, rendered.missing);\n continue;\n }\n planned.push(\n planFile({ path: asset.target, asset: asset.id, version, rendered: rendered.text, existing: read(join(root, asset.target)) })\n );\n if (asset.kind === 'rules' && template !== null) {\n companions.push(planCompanion(asset, read(join(root, pathOf(asset))), template));\n }\n }\n\n return {\n planned,\n missing,\n companions,\n abandoned: abandonedOf(config, root, assetsDir),\n gaps: variantGaps(readCatalog(assetsDir), config),\n unbound: unboundHooks(bindingsOf(config, assetsDir), root),\n written: [],\n };\n}\n\n/** Раскладка. Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого. */\nexport function runSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n if (result.missing.size || result.gaps.length || result.planned.some((entry: IPlanned): boolean => isRefusal(entry.outcome))) {\n return result;\n }\n\n // Право на запуск переносится с файла в пакете. Без него гард отвечает отказом доступа, то\n // есть ненулевым кодом, а ненулевой код у гарда значит «правка отбита»: разложенный набор\n // отбивал бы подряд всё, включая сборку и тесты, и причина при этом нигде не называлась.\n const executable: ReadonlySet<string> = new Set(\n collectAssets(config, assetsDir)\n .filter((asset: IAsset): boolean => asset.executable)\n .map((asset: IAsset): string => asset.id)\n );\n\n const written: string[] = [];\n for (const entry of result.planned) {\n if (entry.content === null) {\n continue;\n }\n const path: string = join(root, entry.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, entry.content, 'utf8');\n if (executable.has(entry.asset)) {\n chmodSync(path, 0o755);\n }\n written.push(entry.path);\n }\n\n // Черновик компаньона кладётся только там, где файла нет вовсе. Он без шапки и без суммы:\n // сверять в нём нечего — с первой правки проекта это его текст, а не текст пакета.\n for (const companion of result.companions) {\n if (companion.content === null) {\n continue;\n }\n const path: string = join(root, companion.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, companion.content, 'utf8');\n written.push(companion.path);\n }\n\n return { ...result, written };\n}\n\nexport const pendingOf: (result: ISyncResult) => readonly IPlanned[] = (result: ISyncResult): readonly IPlanned[] =>\n result.planned.filter((entry: IPlanned): boolean => isPending(entry.outcome));\n"]}
@@ -0,0 +1,44 @@
1
+ export interface IOptionOfAxis {
2
+ /** Значение, которое проект пишет в конфиг и которое стоит в имени файла: `github`. */
3
+ readonly value: string;
4
+ /** Чем этот вид отличается от соседнего — им человек и выбирает. */
5
+ readonly title: string;
6
+ }
7
+ export interface IAxis {
8
+ /** Имя оси, оно же ключ в `variants` конфига: `host`. */
9
+ readonly name: string;
10
+ /** Вопрос, которым ось спрашивают при заведении конфига. */
11
+ readonly question: string;
12
+ /** Чем ось названа в перечнях и отказах. */
13
+ readonly title: string;
14
+ readonly options: readonly IOptionOfAxis[];
15
+ }
16
+ /** Ось и вид, которым помечен ресурс. У ресурса без пометки его нет. */
17
+ export interface IVariant {
18
+ readonly axis: string;
19
+ readonly value: string;
20
+ }
21
+ export declare const VARIANTS_FILE: string;
22
+ /** Оси, объявленные пакетом. Нет файла — осей нет, и все ресурсы считаются общими. */
23
+ export declare function readAxes(assetsDir: string): readonly IAxis[];
24
+ /**
25
+ * Чем помечено имя файла: `git-workflow.github.md` — видом `github` оси `host`.
26
+ *
27
+ * Вид ищется среди объявленных значений, а не по одному лишь виду имени. Иначе `app.config.md`
28
+ * прочитался бы как ресурс `app` вида `config`, и раскладка потеряла бы файл молча — по
29
+ * причине, которую в имени не разглядеть.
30
+ */
31
+ export declare function variantOf(file: string, axes: readonly IAxis[]): IVariant | null;
32
+ /** Имя файла без пометки вида: под ним ресурс и ложится в дерево проекта. */
33
+ export declare function withoutVariant(file: string, variant: IVariant | null): string;
34
+ /**
35
+ * Взят ли ресурс при этом выборе видов.
36
+ *
37
+ * Ресурс без пометки берётся всегда. Помеченный — только когда проект назвал ровно этот вид:
38
+ * невыбранная ось значит, что дерево о ней ничего не сказало, и класть ему наугад один из трёх
39
+ * git-flow хуже, чем не класть ни одного.
40
+ */
41
+ export declare function matchesVariant(variant: IVariant | null, chosen: Readonly<Record<string, string>>): boolean;
42
+ /** Оси, о которых проект молчит, хотя пакет их объявил, — по ним раскладка не полна. */
43
+ export declare function unansweredAxes(axes: readonly IAxis[], chosen: Readonly<Record<string, string>>): readonly IAxis[];
44
+ //# sourceMappingURL=variants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/variants.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,aAAa;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,KAAK;IAClB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AAED,wEAAwE;AACxE,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,aAAa,EAAE,MAAwB,CAAC;AAErD,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,EAAE,CAiB5D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,KAAK,EAAE,GAAG,QAAQ,GAAG,IAAI,CAc/E;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,GAAG,MAAM,CAQ7E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAE1G;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,KAAK,EAAE,CAIjH"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Оси различия между деревьями и выбор проекта по ним.
3
+ *
4
+ * Есть приёмы, которые везде одни, и есть такие, у которых один и тот же закон исполняется
5
+ * разными командами: заявка на слияние открывается `gh`, `glab` или `az repos` — в зависимости
6
+ * от того, где лежит репозиторий. Обезличить их до общего текста нельзя: правило, из которого
7
+ * убрали команду, перестаёт быть указанием и становится пожеланием.
8
+ *
9
+ * Поэтому такой ресурс лежит в пакете в нескольких видах — `git-workflow.github.md`,
10
+ * `git-workflow.gitlab.md`, `git-workflow.azure.md`, — а проект выбирает вид один раз, при
11
+ * заведении конфига. Разложен будет ровно один, под общим именем: агент читает `git-workflow`,
12
+ * а какой это хостинг, ему знать незачем.
13
+ *
14
+ * Ось объявлена пакетом, а не выдумана на месте: значение из конфига, которому ни одна ось не
15
+ * отвечает, — это опечатка, и молчать о ней нельзя.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ export const VARIANTS_FILE = 'variants.json';
20
+ /** Оси, объявленные пакетом. Нет файла — осей нет, и все ресурсы считаются общими. */
21
+ export function readAxes(assetsDir) {
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(readFileSync(join(assetsDir, VARIANTS_FILE), 'utf8'));
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ if (typeof raw !== 'object' || raw === null) {
30
+ return [];
31
+ }
32
+ return Object.entries(raw).map(([name, axis]) => ({
33
+ name,
34
+ question: axis.question,
35
+ title: axis.title,
36
+ options: axis.options ?? [],
37
+ }));
38
+ }
39
+ /**
40
+ * Чем помечено имя файла: `git-workflow.github.md` — видом `github` оси `host`.
41
+ *
42
+ * Вид ищется среди объявленных значений, а не по одному лишь виду имени. Иначе `app.config.md`
43
+ * прочитался бы как ресурс `app` вида `config`, и раскладка потеряла бы файл молча — по
44
+ * причине, которую в имени не разглядеть.
45
+ */
46
+ export function variantOf(file, axes) {
47
+ const parts = file.split('.');
48
+ if (parts.length < 3) {
49
+ return null;
50
+ }
51
+ const spoken = parts[parts.length - 2];
52
+ for (const axis of axes) {
53
+ if (axis.options.some((option) => option.value === spoken)) {
54
+ return { axis: axis.name, value: spoken };
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ /** Имя файла без пометки вида: под ним ресурс и ложится в дерево проекта. */
60
+ export function withoutVariant(file, variant) {
61
+ if (!variant) {
62
+ return file;
63
+ }
64
+ const parts = file.split('.');
65
+ parts.splice(parts.length - 2, 1);
66
+ return parts.join('.');
67
+ }
68
+ /**
69
+ * Взят ли ресурс при этом выборе видов.
70
+ *
71
+ * Ресурс без пометки берётся всегда. Помеченный — только когда проект назвал ровно этот вид:
72
+ * невыбранная ось значит, что дерево о ней ничего не сказало, и класть ему наугад один из трёх
73
+ * git-flow хуже, чем не класть ни одного.
74
+ */
75
+ export function matchesVariant(variant, chosen) {
76
+ return variant === null || chosen[variant.axis] === variant.value;
77
+ }
78
+ /** Оси, о которых проект молчит, хотя пакет их объявил, — по ним раскладка не полна. */
79
+ export function unansweredAxes(axes, chosen) {
80
+ return axes.filter((axis) => !axis.options.some((option) => option.value === chosen[axis.name]));
81
+ }
82
+ //# sourceMappingURL=variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/variants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAyBjC,MAAM,CAAC,MAAM,aAAa,GAAW,eAAe,CAAC;AAErD,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,SAAiB;IACtC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACD,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO,EAAE,CAAC;IACd,CAAC;IAED,OAAO,MAAM,CAAC,OAAO,CAAC,GAA4B,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAkB,EAAS,EAAE,CAAC,CAAC;QAC/F,IAAI;QACJ,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;KAC9B,CAAC,CAAC,CAAC;AACR,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAsB;IAC1D,MAAM,KAAK,GAAsB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,MAAM,MAAM,GAAW,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE/C,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACtB,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAqB,EAAW,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,CAAC;YACjF,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAC9C,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAwB;IACjE,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,MAAM,KAAK,GAAa,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAElC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAwB,EAAE,MAAwC;IAC7F,OAAO,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC;AACtE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,cAAc,CAAC,IAAsB,EAAE,MAAwC;IAC3F,OAAO,IAAI,CAAC,MAAM,CACd,CAAC,IAAW,EAAW,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAqB,EAAW,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CACvH,CAAC;AACN,CAAC","sourcesContent":["/**\n * Оси различия между деревьями и выбор проекта по ним.\n *\n * Есть приёмы, которые везде одни, и есть такие, у которых один и тот же закон исполняется\n * разными командами: заявка на слияние открывается `gh`, `glab` или `az repos` — в зависимости\n * от того, где лежит репозиторий. Обезличить их до общего текста нельзя: правило, из которого\n * убрали команду, перестаёт быть указанием и становится пожеланием.\n *\n * Поэтому такой ресурс лежит в пакете в нескольких видах — `git-workflow.github.md`,\n * `git-workflow.gitlab.md`, `git-workflow.azure.md`, — а проект выбирает вид один раз, при\n * заведении конфига. Разложен будет ровно один, под общим именем: агент читает `git-workflow`,\n * а какой это хостинг, ему знать незачем.\n *\n * Ось объявлена пакетом, а не выдумана на месте: значение из конфига, которому ни одна ось не\n * отвечает, — это опечатка, и молчать о ней нельзя.\n */\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport interface IOptionOfAxis {\n /** Значение, которое проект пишет в конфиг и которое стоит в имени файла: `github`. */\n readonly value: string;\n /** Чем этот вид отличается от соседнего — им человек и выбирает. */\n readonly title: string;\n}\n\nexport interface IAxis {\n /** Имя оси, оно же ключ в `variants` конфига: `host`. */\n readonly name: string;\n /** Вопрос, которым ось спрашивают при заведении конфига. */\n readonly question: string;\n /** Чем ось названа в перечнях и отказах. */\n readonly title: string;\n readonly options: readonly IOptionOfAxis[];\n}\n\n/** Ось и вид, которым помечен ресурс. У ресурса без пометки его нет. */\nexport interface IVariant {\n readonly axis: string;\n readonly value: string;\n}\n\nexport const VARIANTS_FILE: string = 'variants.json';\n\n/** Оси, объявленные пакетом. Нет файла — осей нет, и все ресурсы считаются общими. */\nexport function readAxes(assetsDir: string): readonly IAxis[] {\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(join(assetsDir, VARIANTS_FILE), 'utf8'));\n } catch {\n return [];\n }\n if (typeof raw !== 'object' || raw === null) {\n return [];\n }\n\n return Object.entries(raw as Record<string, IAxis>).map(([name, axis]: [string, IAxis]): IAxis => ({\n name,\n question: axis.question,\n title: axis.title,\n options: axis.options ?? [],\n }));\n}\n\n/**\n * Чем помечено имя файла: `git-workflow.github.md` — видом `github` оси `host`.\n *\n * Вид ищется среди объявленных значений, а не по одному лишь виду имени. Иначе `app.config.md`\n * прочитался бы как ресурс `app` вида `config`, и раскладка потеряла бы файл молча — по\n * причине, которую в имени не разглядеть.\n */\nexport function variantOf(file: string, axes: readonly IAxis[]): IVariant | null {\n const parts: readonly string[] = file.split('.');\n if (parts.length < 3) {\n return null;\n }\n const spoken: string = parts[parts.length - 2];\n\n for (const axis of axes) {\n if (axis.options.some((option: IOptionOfAxis): boolean => option.value === spoken)) {\n return { axis: axis.name, value: spoken };\n }\n }\n\n return null;\n}\n\n/** Имя файла без пометки вида: под ним ресурс и ложится в дерево проекта. */\nexport function withoutVariant(file: string, variant: IVariant | null): string {\n if (!variant) {\n return file;\n }\n const parts: string[] = file.split('.');\n parts.splice(parts.length - 2, 1);\n\n return parts.join('.');\n}\n\n/**\n * Взят ли ресурс при этом выборе видов.\n *\n * Ресурс без пометки берётся всегда. Помеченный — только когда проект назвал ровно этот вид:\n * невыбранная ось значит, что дерево о ней ничего не сказало, и класть ему наугад один из трёх\n * git-flow хуже, чем не класть ни одного.\n */\nexport function matchesVariant(variant: IVariant | null, chosen: Readonly<Record<string, string>>): boolean {\n return variant === null || chosen[variant.axis] === variant.value;\n}\n\n/** Оси, о которых проект молчит, хотя пакет их объявил, — по ним раскладка не полна. */\nexport function unansweredAxes(axes: readonly IAxis[], chosen: Readonly<Record<string, string>>): readonly IAxis[] {\n return axes.filter(\n (axis: IAxis): boolean => !axis.options.some((option: IOptionOfAxis): boolean => option.value === chosen[axis.name])\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/agent-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Переносимый слой правил для агента: законы, хуки, проверки и агенты, раскладываемые в репозиторий одной командой",
5
5
  "author": "RT Team",
6
6
  "license": "Apache-2.0",
Binary file
@@ -1,35 +0,0 @@
1
- # Список записей в админке
2
-
3
- Как владелец видит набор записей и что с ними делает, не открывая ни одну: заявки, объекты,
4
- адреса страниц, промокоды, подписки на календарь. Правила общие для всех разделов — список,
5
- устроенный иначе остальных, владелец читает заново.
6
-
7
- Открытая запись, попадание в раздел и объём данных на строку — предмет других законов.
8
-
9
- ## Статьи
10
-
11
- - **Записи показаны таблицей: строка — запись, столбец — её свойство.**
12
- - **Клик по строке открывает запись.** Меню строки — для действий над ней, а не для входа.
13
- - **Пока список читается, на месте строк — скелетоны.** Пустая область неотличима от списка,
14
- в котором ничего нет.
15
- - **Пустой список объясняет, почему он пуст.**
16
- - **На узком экране строка становится карточкой.**
17
- - **Состав и порядок столбцов задаёт владелец, и выбор сохраняется между заходами.**
18
- - **Слева в тулбаре — то, что меняет выборку, справа — действия над списком.**
19
- - **Действие, недоступное записи, в её меню не показано.**
20
- - **У записи, которой нельзя сделать ничего, кнопки меню нет.**
21
- - **Действие, которое нельзя отменить, спрашивает подтверждение и называет последствие.**
22
- - **Отказ загрузки подаётся тостом, а повторить владелец может кнопкой обновления.**
23
- - **Список показывает тот порядок и тот отбор, которые применены к записям.** Порядок по
24
- умолчанию виден стрелкой сразу, а условие, которого сервер не принял, не остаётся в тулбаре
25
- как действующее.
26
- - **Заголовки столбцов, тексты состояний и подписи действий приходят из словарей.**
27
- - **Вид списка живёт в адресе: отбор, порядок и номер страницы.** Иначе ссылку на отобранный
28
- список не передать, перезагрузка возвращает список к умолчанию, а возврат из открытой записи
29
- сбрасывает набранное. Размер страницы и состав столбцов в адресе не участвуют: они про
30
- владельца, а не про то, что он показывает другому.
31
- - **Форма запроса списка одна на все разделы, и ответ несёт общее число записей.** Переписанная
32
- в каждом разделе, она расходится по одному полю за раз; без общего числа не показать, сколько
33
- страниц.
34
- - **Выпадающий список берётся той же страницей, что и таблица, и догружается прокруткой.**
35
- Страниц в нём не видно, значит и переключать их нечем.
@@ -1,38 +0,0 @@
1
- # Навигация админки
2
-
3
- Как владелец находит раздел и попадает в него. Правила держат все разделы админки сразу —
4
- заявки, объекты, календарь, переписку, аналитику, цены, организацию, — и разойтись им нельзя:
5
- раздел, который ведёт себя иначе остальных, выглядит сломанным.
6
-
7
- ## Статьи
8
-
9
- - **Пункт меню ведёт на свой экран.** Не на вкладку внутри чужого экрана и не на панель: адрес
10
- открывается по прямой ссылке, переживает перезагрузку и передаётся другому человеку.
11
- - **Раздел, ни один пункт которого владельцу не виден, в меню не показан.**
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
- наверх возвращаются кнопкой браузера, а из открытого по ссылке экрана — некуда.
@@ -1,175 +0,0 @@
1
- ---
2
- name: git-workflow-commit
3
- kind: pattern
4
- rule: git-workflow
5
- description: Паттерн правила git-workflow. Брать на заведение задачи, ветки, коммит, пуш и создание PR — план до первой задачи, заведение задачи всеми шагами сразу, перевод по колонкам, слияние двух задач в одну, работа от учётной записи машинной работы, формат заголовка, строка связи с задачей, состав PR, чеклист проверок до публикации. Не брать для миграций и перезапуска прода — это паттерны git-workflow-migration и git-workflow-restart.
6
- ---
7
-
8
- # Ветка, коммит и PR
9
-
10
- Паттерн правила `git-workflow`. Что при этом должно быть верно — закон `{{lawsDir}}/delivery.md`.
11
-
12
- ## Когда брать
13
-
14
- - Заводится задача, с которой начинается правка.
15
- - Заводится ветка под задачу.
16
- - Готовится коммит или пуш.
17
- - Открывается PR.
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
- Название задачи говорит, что не так, а не что сделать: PR потом переводит его в сделанное.
43
- Номер в заголовок руками не пишется — он известен только после создания.
44
-
45
- ## Две задачи, которые чинятся одной правкой
46
-
47
- Если по ходу выяснилось, что правка закрывает и соседнюю задачу, — это одна задача, а не две.
48
- Слить их можно, пока правка не въехала в главную ветку: недостающее из поглощённой дописывается
49
- в тело первой, и только потом поглощённая закрывается и снимается с очереди. Порядок важен —
50
- удаление уносит с собой ссылки на неё из чужих тел.
51
-
52
- После слияния ветки поглощения нет: она въехала, и откатывается целиком.
53
-
54
- ## Ветка заводится отдельным вызовом
55
-
56
- Гард главной ветки разбирает текст команды и смотрит ветку на момент запуска, поэтому составная
57
- команда отклоняется целиком — ветки в ней ещё нет:
58
-
59
- ```bash
60
- ✗ git checkout -b <ветка> && git commit -m '…'
61
- ✓ git checkout -b <ветка>
62
- ✓ git commit -F -
63
- ```
64
-
65
- Имя ветки несёт номер задачи. Гард поставки разбирает его на месте и отбивает промах в форме до
66
- первого коммита, а по номеру спрашивает очередь: задача должна существовать, быть открытой,
67
- стоять в очереди и иметь исполнителя.
68
-
69
- Имя без номера законно, пока ветка живёт локально — под пробу и разбор. PR с неё не откроется:
70
- правка, доезжающая до главной ветки, начинается с задачи.
71
-
72
- ## Колонка задачи двигается вместе с работой
73
-
74
- Ветка заведена — задача уже не в начальной колонке, а в работе. PR открыт — она ждёт разбора.
75
- Оба перевода делает одна команда, вторым вызовом сразу за тем, который его вызвал.
76
-
77
- Перевод не откладывается на потом: очередь читают между шагами, а не после них. Задача с
78
- открытым PR, простоявшая в начальной колонке, всё это время выглядела нетронутой — и разбора за
79
- неё никто не ждал.
80
-
81
- ## Коммит подписывается учётной записью машинной работы
82
-
83
- Токен читается в переменную и не печатается; автор и коммиттер задаются переменными той же
84
- команды. Правка общей настройки здесь не годится — она переписала бы подпись владельцу:
85
-
86
- ```bash
87
- TOKEN=$(tr -d '\n' < <файл с токеном>)
88
-
89
- GIT_AUTHOR_NAME="<бот>" GIT_AUTHOR_EMAIL="<адрес бота>" \
90
- GIT_COMMITTER_NAME="<бот>" GIT_COMMITTER_EMAIL="<адрес бота>" \
91
- git commit -F -
92
- ```
93
-
94
- Заголовок — `тип(область): описание`, без точки в конце. Набор типов и областей задан
95
- настройкой проверки заголовка.
96
-
97
- ## Документ едет тем же коммитом
98
-
99
- Гард документов требует пару и называет её сам. Обход — строка в теле коммита, причина
100
- обязательна:
101
-
102
- ```
103
- Docs-skip: правка только в сценариях хука, зеркала у него нет
104
- ```
105
-
106
- ## Номер задачи стоит в её заголовке и в заголовке PR
107
-
108
- Форма одна на оба. Номер стоит в самом заголовке, а не только в теле: в списке PR тела не
109
- видно. Тот же номер несёт и имя ветки — поэтому задача, ветка и PR читаются как одно.
110
-
111
- Задача говорит, что не так; PR тем же номером отчитывается, что сделано. Инфинитив из задачи в
112
- заголовок PR не переносится: «исправить» становится «исправлено».
113
-
114
- Тип и область коммита в заголовок PR не идут: род правки и область уже видны метками.
115
-
116
- ## PR прикрепляется к задаче
117
-
118
- Тело начинается со строки связи с задачей — по ней в очереди заполняется поле связанных PR.
119
- Ревьювер, исполнитель и метки задаются той же командой, и PR без них не открывается.
120
-
121
- Ревьювер — всегда владелец: без запроса разбора PR не показывается ему в очереди. Метки берутся
122
- у задачи целиком — и род правки, и все её области; читаются они у задачи, а не выбираются по
123
- памяти.
124
-
125
- Строка связи обязательна: без неё PR не прикрепляется к задаче. Она же означает, что задача
126
- закрывается целиком — половину задачи одним PR не выкатывают: у задачи одна ветка, и работа,
127
- которая в неё не влезает, делится на задачи до того, как ветка заводится.
128
-
129
- Тело перечитывается всякий раз, когда в ветку что-то влилось после публикации: отчёт утверждает
130
- про дерево, а дерево с тех пор изменилось.
131
-
132
- ## Что проверяется до публикации PR
133
-
134
- Проверок на самом PR нет: выкатка запускается пушем в главную ветку, и до слияния никто не
135
- гоняет ничего. Линтеры и юниты снимает гейт пуша — ниже то, чего он не знает.
136
-
137
- 1. **В ветке только та правка, за которой её заводили** — сводка расхождения с главной веткой.
138
- Чужой домен в списке файлов означает, что правка расползлась.
139
- 2. **Ни мока, ни подменённого ответа, ни отладочной строки** — расхождение читается целиком, а
140
- не по именам файлов. На прод они уезжают молча и портят настоящие данные.
141
- 3. **Документ едет тем же коммитом.** Пару называет гард, но спек домена и правку его поведения
142
- он не знает — это остаётся за автором.
143
- 4. **Проверки текстов и раскладки зелёные.**
144
- 5. **Все приложения собираются.** Гейт пуша сборку обычно не гоняет.
145
- 6. **Видимый текст заведён во всех локалях.**
146
- 7. **Правка вёрстки подтверждена замером**, а не взглядом, и снята при узком экране — паттерн
147
- `browser-verification-measure`.
148
- 8. **Правка публичной разметки проверена на прод-сборке по всем локалям** — паттерн
149
- `seo-verify`.
150
- 9. **Тело PR начинается строкой связи с задачей**, а метки, ревьювер и исполнитель стоят.
151
- 10. **Заголовок PR несёт номер задачи и называет её сделанной** — тем же номером, что у задачи
152
- и в имени ветки.
153
- 11. **Очередь работ сходится.** Задача в очереди, с исполнителем и номером в заголовке; PR один
154
- на задачу, и закрывает он её целиком.
155
-
156
- Сразу после публикации задача переставляется в разбор, и сверка очереди прогоняется ещё раз: до
157
- открытия PR колонку она не судит, а после открытия расхождение видит.
158
-
159
- Сделанное рассуждением и сделанное замером в теле PR разводятся прямо: непроверенное, названное
160
- проверенным, ревьювер принимает за проверенное.
161
-
162
- ## Частые промахи
163
-
164
- - **Добавление в индекс нескольких путей не добавляет ничего, если хоть один путь не
165
- существует.** Команда обрывается на первом промахе целиком, а следующая правка последнего
166
- коммита уносит в него всё, что осталось в индексе. Состав коммита читается сразу после него,
167
- а не на разборе PR.
168
- - **Задача, заведённая мимо команды, в очередь не попадает и гардом не отбивается** — он
169
- смотрит команду, а не задачу. Ловится это только сверкой очереди.
170
- - **Исполнитель у задачи сам не проставляется** ни при заведении через веб, ни при добавлении в
171
- очередь.
172
- - **Колонка сама не двигается** ни от заведения ветки, ни от открытия PR: очередь ветки не
173
- видит вовсе, а связь с PR заполняет только поле связанных PR.
174
- - **Постраничный обход очереди через общий флаг уходит в повтор первой страницы** — курсор
175
- берётся из ответа руками, а полнота сверяется с общим числом элементов.