@rt-tools/agent-kit 0.8.2 → 0.9.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 (170) hide show
  1. package/README.md +13 -0
  2. package/assets/agents/rules-reviewer.md +83 -0
  3. package/assets/checks/board.github.mjs +48 -1
  4. package/assets/checks/check-board.github.mjs +84 -1
  5. package/assets/checks/check-dupes.mjs +66 -6
  6. package/assets/checks/check-lib-layers.mjs +13 -524
  7. package/assets/checks/check-specs.mjs +61 -747
  8. package/assets/checks/check-styles.mjs +185 -15
  9. package/assets/checks/lib-boundaries.mjs +143 -0
  10. package/assets/checks/lib-common.mjs +149 -0
  11. package/assets/checks/lib-domains.mjs +205 -0
  12. package/assets/checks/lib-manifests.mjs +60 -0
  13. package/assets/checks/lib-reexports.mjs +101 -0
  14. package/assets/checks/rt-kit-checks.config.mjs +26 -0
  15. package/assets/checks/spec-anchors.mjs +297 -0
  16. package/assets/checks/spec-common.mjs +222 -0
  17. package/assets/checks/spec-contract.mjs +152 -0
  18. package/assets/checks/spec-scenarios.mjs +201 -0
  19. package/assets/commands/feedback.md +95 -0
  20. package/assets/commands/rules-review.md +98 -0
  21. package/assets/commands/skill-curator.md +39 -22
  22. package/assets/defaults/project.sh +8 -0
  23. package/assets/docs/GLOSSARY.md +21 -20
  24. package/assets/hooks/git-guard-push-tests.sh +8 -4
  25. package/assets/hooks/reuse-first-guard.sh +16 -2
  26. package/assets/hooks/skill-gate.sh +1 -1
  27. package/assets/hooks/sql-guard-parse.sh +187 -0
  28. package/assets/hooks/sql-guard-request.sh +117 -0
  29. package/assets/hooks/sql-guard-target.sh +134 -0
  30. package/assets/hooks/sql-guard-write.sh +212 -0
  31. package/assets/hooks/sql-guard.sh +26 -596
  32. package/assets/hooks/task-flow-guard.sh +16 -2
  33. package/assets/hooks/waiting-turn-guard.sh +116 -0
  34. package/assets/laws/delivery.md +35 -0
  35. package/assets/laws/project-documentation.md +18 -0
  36. package/assets/laws/work-conduct.md +25 -0
  37. package/assets/patterns/admin-lists-screen.md +25 -14
  38. package/assets/patterns/admin-nav-item.md +1 -1
  39. package/assets/patterns/component-structure-new.md +1 -1
  40. package/assets/patterns/entity-aside.md +4 -2
  41. package/assets/patterns/git-workflow-commit.azure.md +74 -2
  42. package/assets/patterns/git-workflow-commit.github.md +75 -2
  43. package/assets/patterns/git-workflow-commit.gitlab.md +75 -4
  44. package/assets/patterns/git-workflow-docker.md +30 -0
  45. package/assets/patterns/observability-record.md +9 -0
  46. package/assets/patterns/shared-code-new.md +2 -2
  47. package/assets/patterns/task-flow-close.md +160 -47
  48. package/assets/patterns/task-flow-handoff.md +1 -1
  49. package/assets/patterns/task-flow-resume.md +3 -3
  50. package/assets/patterns/task-flow-start.md +32 -5
  51. package/assets/rules/angular-patterns.md +22 -0
  52. package/assets/rules/api-layer.md +25 -0
  53. package/assets/rules/browser-verification.md +32 -0
  54. package/assets/rules/component-structure.md +21 -0
  55. package/assets/rules/dependencies.md +22 -0
  56. package/assets/rules/doc-style.md +24 -0
  57. package/assets/rules/entity-conventions.needs-admin.md +21 -0
  58. package/assets/rules/entity-models.md +21 -0
  59. package/assets/rules/git-workflow.azure.md +57 -1
  60. package/assets/rules/git-workflow.github.md +89 -2
  61. package/assets/rules/git-workflow.gitlab.md +56 -1
  62. package/assets/rules/lib-layers.md +29 -0
  63. package/assets/rules/lists.md +37 -10
  64. package/assets/rules/navigation.md +21 -0
  65. package/assets/rules/observability.needs-app.md +23 -0
  66. package/assets/rules/permissions.md +23 -0
  67. package/assets/rules/platform-access.md +21 -0
  68. package/assets/rules/reuse-first.md +20 -0
  69. package/assets/rules/seo.md +19 -0
  70. package/assets/rules/shared-code.md +20 -1
  71. package/assets/rules/spec-driven.md +32 -0
  72. package/assets/rules/styling-bem.md +19 -0
  73. package/assets/rules/task-flow.md +148 -18
  74. package/assets/rules/testing.md +62 -0
  75. package/assets/rules/translations.md +21 -0
  76. package/assets/rules/typescript-conventions.md +28 -0
  77. package/assets/samples/specs/_template/spec.md +83 -0
  78. package/assets/samples/tasks/_template/grill.md +28 -0
  79. package/assets/samples/tasks/_template/plan.md +39 -0
  80. package/assets/samples/tasks/_template/progress.md +23 -0
  81. package/assets/skills/agent-kit.md +52 -2
  82. package/assets/templates/proposal.md +21 -0
  83. package/assets/templates/rule.md +31 -2
  84. package/bin/agent-kit.d.ts.map +1 -1
  85. package/bin/agent-kit.js +115 -87
  86. package/bin/agent-kit.js.map +1 -1
  87. package/index.d.ts +1 -0
  88. package/index.d.ts.map +1 -1
  89. package/index.js +1 -0
  90. package/index.js.map +1 -1
  91. package/lib/argv.d.ts.map +1 -1
  92. package/lib/argv.js +6 -4
  93. package/lib/argv.js.map +1 -1
  94. package/lib/assets.d.ts.map +1 -1
  95. package/lib/assets.js +2 -1
  96. package/lib/assets.js.map +1 -1
  97. package/lib/cargo.d.ts +20 -0
  98. package/lib/cargo.d.ts.map +1 -1
  99. package/lib/cargo.js.map +1 -1
  100. package/lib/cascade.d.ts +55 -0
  101. package/lib/cascade.d.ts.map +1 -0
  102. package/lib/cascade.js +131 -0
  103. package/lib/cascade.js.map +1 -0
  104. package/lib/catalog.d.ts +0 -75
  105. package/lib/catalog.d.ts.map +1 -1
  106. package/lib/catalog.js +44 -127
  107. package/lib/catalog.js.map +1 -1
  108. package/lib/commands.d.ts.map +1 -1
  109. package/lib/commands.js +153 -85
  110. package/lib/commands.js.map +1 -1
  111. package/lib/companion.d.ts.map +1 -1
  112. package/lib/companion.js +5 -5
  113. package/lib/companion.js.map +1 -1
  114. package/lib/config.d.ts +12 -1
  115. package/lib/config.d.ts.map +1 -1
  116. package/lib/config.js +11 -5
  117. package/lib/config.js.map +1 -1
  118. package/lib/enroll.d.ts +56 -0
  119. package/lib/enroll.d.ts.map +1 -0
  120. package/lib/enroll.js +123 -0
  121. package/lib/enroll.js.map +1 -0
  122. package/lib/freshness.d.ts.map +1 -1
  123. package/lib/freshness.js +31 -17
  124. package/lib/freshness.js.map +1 -1
  125. package/lib/hooks-map.d.ts +30 -0
  126. package/lib/hooks-map.d.ts.map +1 -1
  127. package/lib/hooks-map.js +80 -18
  128. package/lib/hooks-map.js.map +1 -1
  129. package/lib/integrity.d.ts +1 -2
  130. package/lib/integrity.d.ts.map +1 -1
  131. package/lib/integrity.js +0 -1
  132. package/lib/integrity.js.map +1 -1
  133. package/lib/observations.d.ts.map +1 -1
  134. package/lib/observations.js +25 -12
  135. package/lib/observations.js.map +1 -1
  136. package/lib/order.d.ts +10 -0
  137. package/lib/order.d.ts.map +1 -0
  138. package/lib/order.js +14 -0
  139. package/lib/order.js.map +1 -0
  140. package/lib/picker.d.ts.map +1 -1
  141. package/lib/picker.js +8 -2
  142. package/lib/picker.js.map +1 -1
  143. package/lib/plan.js +1 -1
  144. package/lib/plan.js.map +1 -1
  145. package/lib/proposals.d.ts.map +1 -1
  146. package/lib/proposals.js +25 -8
  147. package/lib/proposals.js.map +1 -1
  148. package/lib/sections.js +1 -1
  149. package/lib/sections.js.map +1 -1
  150. package/lib/ship.d.ts.map +1 -1
  151. package/lib/ship.js +9 -1
  152. package/lib/ship.js.map +1 -1
  153. package/lib/shipment.d.ts.map +1 -1
  154. package/lib/shipment.js +14 -10
  155. package/lib/shipment.js.map +1 -1
  156. package/lib/snapshot.d.ts.map +1 -1
  157. package/lib/snapshot.js +2 -1
  158. package/lib/snapshot.js.map +1 -1
  159. package/lib/stamp.js +1 -1
  160. package/lib/stamp.js.map +1 -1
  161. package/lib/sync.d.ts +12 -2
  162. package/lib/sync.d.ts.map +1 -1
  163. package/lib/sync.js +11 -10
  164. package/lib/sync.js.map +1 -1
  165. package/lib/vars.d.ts.map +1 -1
  166. package/lib/vars.js +2 -3
  167. package/lib/vars.js.map +1 -1
  168. package/package.json +1 -1
  169. package/rt-tools-agent-kit-0.9.0.tgz +0 -0
  170. package/rt-tools-agent-kit-0.8.2.tgz +0 -0
package/lib/commands.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines -- файл делится задачей RT-849 */
1
2
  /**
2
3
  * Команды и их вывод. Печать отделена от работы: команда возвращает строки и код возврата,
3
4
  * а `process.exit` зовёт только точка входа — иначе ни одну из них нельзя было бы проверить
@@ -6,7 +7,8 @@
6
7
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
7
8
  import { join } from 'node:path';
8
9
  import { collectAssets } from './assets.js';
9
- import { cascadeCuts, isChosen, readCatalog } from './catalog.js';
10
+ import { cascadeCuts } from './cascade.js';
11
+ import { isChosen, readCatalog } from './catalog.js';
10
12
  import { debtLine, isUnfilled, pathOf as companionPathOf, unaddressedOf } from './companion.js';
11
13
  import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, PROFILE_FILE, readConfig, RT_KIT_DIR } from './config.js';
12
14
  import { hooksSection, SETTINGS_PATH } from './hooks-map.js';
@@ -17,6 +19,7 @@ import { pendingOf, planSync, runSync } from './sync.js';
17
19
  import { answersRequirement, readTraits, unknownTraits } from './traits.js';
18
20
  import { placeholdersOf } from './vars.js';
19
21
  import { readAxes, unansweredAxes } from './variants.js';
22
+ import { byText } from './order.js';
20
23
  /** Что с файлом сделала раскладка. Прошедшее время здесь правда: `sync` уже записал. */
21
24
  const DONE_WORD = {
22
25
  create: 'положен',
@@ -58,6 +61,7 @@ const KIND_TITLE = {
58
61
  workflows: 'ВОРКФЛОУ',
59
62
  templates: 'ШАБЛОНЫ',
60
63
  docs: 'ДОКУМЕНТЫ',
64
+ samples: 'ОБРАЗЦЫ',
61
65
  };
62
66
  /** Состояние компаньона словами. У заполненного слова нет: о нём говорить нечего. */
63
67
  const COMPANION_WORD = {
@@ -82,20 +86,9 @@ const PROFILE_HELPER = 'rt_needs';
82
86
  * есть.
83
87
  */
84
88
  const PROFILE_DEFINE = /(rt_[a-z_]+)\s*\(\)\s*\{/g;
85
- /**
86
- * Функции профиля, которых ждут взятые хуки, и те из них, что дерево не определило.
87
- *
88
- * Отдельной строкой в разборе состояния потому, что на месте вызова нехватка не видна вовсе:
89
- * хук выходит с нулём, стоит в настройке, виден в списке — и читается как работающий. Тишина
90
- * при этом означает разом «нечего проверять», «проверять нечем» и «всё в порядке».
91
- */
92
- function profileLines(root, assetsDir, config) {
93
- const wanted = new Set();
94
- const defined = new Set();
95
- for (const asset of collectAssets(config, assetsDir)) {
96
- if (asset.kind !== 'hooks') {
97
- continue;
98
- }
89
+ /** Что взятые хуки зовут у профиля и что определяют сами. */
90
+ function readHooks(assets, wanted, defined) {
91
+ for (const asset of assets.filter((one) => one.kind === 'hooks')) {
99
92
  for (const found of asset.text.matchAll(PROFILE_CALL)) {
100
93
  if (found[1] !== PROFILE_HELPER) {
101
94
  wanted.add(found[1]);
@@ -108,23 +101,36 @@ function profileLines(root, assetsDir, config) {
108
101
  defined.add(found[1]);
109
102
  }
110
103
  }
111
- if (!wanted.size) {
112
- return [];
113
- }
104
+ }
105
+ /** Функции, определённые самим профилем: разложенным умолчанием пакета и надстройкой дерева. */
106
+ function readProfiles(root, config, defined) {
114
107
  // Профиль собирается из тех же файлов, что читает сам хук: сперва разложенное умолчание
115
108
  // пакета, поверх — надстройка дерева. Судить по одной надстройке значило бы объявить мёртвым
116
109
  // каждый гард дерева, которое умолчаний не переписывало.
117
110
  const defaults = config.layout.defaults ?? DEFAULT_LAYOUT.defaults;
118
111
  const sources = [join(root, defaults, PROFILE_FILE), join(root, RT_KIT_DIR, PROFILE_FILE)];
119
- for (const path of sources) {
120
- if (!existsSync(path)) {
121
- continue;
122
- }
112
+ for (const path of sources.filter((one) => existsSync(one))) {
123
113
  for (const found of readFileSync(path, 'utf8').matchAll(PROFILE_DEFINE)) {
124
114
  defined.add(found[1]);
125
115
  }
126
116
  }
127
- const absent = [...wanted].filter((name) => !defined.has(name)).sort();
117
+ }
118
+ /**
119
+ * Функции профиля, которых ждут взятые хуки, и те из них, что дерево не определило.
120
+ *
121
+ * Отдельной строкой в разборе состояния потому, что на месте вызова нехватка не видна вовсе:
122
+ * хук выходит с нулём, стоит в настройке, виден в списке — и читается как работающий. Тишина
123
+ * при этом означает разом «нечего проверять», «проверять нечем» и «всё в порядке».
124
+ */
125
+ function profileLines(root, assetsDir, config) {
126
+ const wanted = new Set();
127
+ const defined = new Set();
128
+ readHooks([...collectAssets(config, assetsDir)], wanted, defined);
129
+ if (!wanted.size) {
130
+ return [];
131
+ }
132
+ readProfiles(root, config, defined);
133
+ const absent = [...wanted].filter((name) => !defined.has(name)).sort(byText);
128
134
  return [
129
135
  `функции профиля, которых ждут взятые хуки: ${wanted.size}`,
130
136
  ...(absent.length
@@ -135,7 +141,14 @@ function profileLines(root, assetsDir, config) {
135
141
  : [' все определены']),
136
142
  ];
137
143
  }
138
- const holes = (result) => [...result.missing].map(([asset, names]) => ` ${asset}: нет значений для ${names.map((name) => `{{${name}}}`).join(', ')}`);
144
+ /** Дырка в тексте пакета, как она написана в самом тексте. */
145
+ function holeOf(name) {
146
+ return `{{${name}}}`;
147
+ }
148
+ const holes = (result) => [...result.missing].map(([asset, names]) => {
149
+ const written = names.map(holeOf).join(', ');
150
+ return ` ${asset}: нет значений для ${written}`;
151
+ });
139
152
  const unfilled = (result) => result.companions.filter(isUnfilled);
140
153
  /**
141
154
  * Строка про компаньон, которому нечего сказать.
@@ -200,13 +213,34 @@ function strangeTraits(config, assetsDir) {
200
213
  const unboundLines = (result) => result.unbound.length
201
214
  ? [
202
215
  `гарды разложены, но в \`${SETTINGS_PATH}\` их не зовёт никто: ${result.unbound.length}`,
203
- ...result.unbound.map((binding) => ` ${binding.path} — ${binding.event}${binding.matcher ? ` ${binding.matcher}` : ''}`),
216
+ ...result.unbound.map((binding) => {
217
+ const matcher = binding.matcher ? ' ' + binding.matcher : '';
218
+ return ` ${binding.path} — ${binding.event}${matcher}`;
219
+ }),
204
220
  ` вставь в \`${SETTINGS_PATH}\` раздел \`hooks\` — готовый кусок ниже:`,
205
221
  ...JSON.stringify({ hooks: hooksSection(result.unbound) }, null, 4)
206
222
  .split('\n')
207
223
  .map((line) => ` ${line}`),
208
224
  ]
209
225
  : [];
226
+ /**
227
+ * Гарды, подписанные не на то, что объявляют.
228
+ *
229
+ * Называются обе стороны: объявленное гардом и стоящее в настройке. Одной стороной такую строку
230
+ * не починить — правится настройка, а верное значение лежит в гарде, и по разнице видно, что
231
+ * именно до него не доходит.
232
+ */
233
+ const driftedLines = (result) => result.drifted.length
234
+ ? [
235
+ `гарды подписаны не на то, что объявляют: ${result.drifted.length}`,
236
+ ...result.drifted.flatMap((drift) => [
237
+ ` ${drift.path} — ${drift.event}`,
238
+ ` объявлено: ${drift.declared || '(без образца)'}`,
239
+ ` в настройке: ${drift.bound || '(без образца)'}`,
240
+ ]),
241
+ ` поправь образец в \`${SETTINGS_PATH}\` по объявлению гарда: тело его разбирает то, что объявлено`,
242
+ ]
243
+ : [];
210
244
  /**
211
245
  * Разорванные связи между ресурсами.
212
246
  *
@@ -230,7 +264,10 @@ const brokenLines = (result) => result.broken.length
230
264
  const idleLines = (result) => result.idle.length
231
265
  ? [
232
266
  `строк отказа, которые ничего не снимают: ${result.idle.length}`,
233
- ...result.idle.map((one) => ` ${one.id} — ${one.by ? `снято отказом от ${one.by}` : 'нет в пакете'}`),
267
+ ...result.idle.map((one) => {
268
+ const why = one.by ? 'снято отказом от ' + one.by : 'нет в пакете';
269
+ return ` ${one.id} — ${why}`;
270
+ }),
234
271
  ' это предупреждение, а не отказ: строки убирает дерево, и раскладка идёт дальше',
235
272
  ]
236
273
  : [];
@@ -293,6 +330,7 @@ const describe = (result) => [
293
330
  ...holes(result),
294
331
  ...gapLines(result),
295
332
  ...unboundLines(result),
333
+ ...driftedLines(result),
296
334
  ...warnings(result),
297
335
  ...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
298
336
  ...unfilled(result).map(companionLine),
@@ -310,7 +348,7 @@ function placeholdersIn(config, assetsDir) {
310
348
  }
311
349
  }
312
350
  }
313
- return names.sort();
351
+ return names.sort(byText);
314
352
  }
315
353
  /**
316
354
  * Что проект выбрал при заведении конфига. Пустой список — весь набор; выбор приходит сюда
@@ -322,7 +360,7 @@ export function init(root, only = [], variants = {}, assetsDir = '') {
322
360
  if (existsSync(path)) {
323
361
  return { code: 0, lines: [`${CONFIG_PATH} уже есть — оставлен как есть`] };
324
362
  }
325
- const config = { vars: {}, layout: DEFAULT_LAYOUT, variants, only, skip: [] };
363
+ const config = { vars: {}, layout: DEFAULT_LAYOUT, skip: [], variants, only };
326
364
  mkdirSync(join(root, OVERRIDES_DIR), { recursive: true });
327
365
  mkdirSync(join(root, CONFIG_PATH, '..'), { recursive: true });
328
366
  writeFileSync(path, `${JSON.stringify(config, null, 4)}\n`, 'utf8');
@@ -380,30 +418,34 @@ const staleRefusal = (stale) => ({
380
418
  ' собери пакет и повтори',
381
419
  ],
382
420
  });
383
- export function sync(env, check) {
384
- const { root, version, assetsDir } = env;
385
- const config = readConfig(root);
386
- if (!config) {
387
- return { code: 1, lines: [NO_CONFIG] };
388
- }
421
+ /**
422
+ * Отказ, случившийся до первой записи на диск. Пусто — раскладке ничто не мешает.
423
+ *
424
+ * Ось без ответа отбивает раскладку целиком, а не пропускает свои ресурсы молча: правило
425
+ * поставки бывает в трёх видах, и дерево, не назвавшее свой, осталось бы вовсе без правила
426
+ * поставки — заметить это можно было бы только по тому, что гейт перестал его требовать.
427
+ *
428
+ * Незнакомое свойство — опечатка, и молчать о ней нельзя ни с одной стороны. У дерева она
429
+ * означает, что помеченного им ресурса оно не получит вовсе; у ресурса — что он не ляжет
430
+ * никуда и никогда, а причину в имени файла не разглядеть.
431
+ */
432
+ function refusalBeforeSync(env, config) {
389
433
  if (env.stale) {
390
434
  return staleRefusal(env.stale);
391
435
  }
392
- // Ось без ответа отбивает раскладку целиком, а не пропускает свои ресурсы молча: правило
393
- // поставки бывает в трёх видах, и дерево, не назвавшее свой, осталось бы вовсе без правила
394
- // поставки — заметить это можно было бы только по тому, что гейт перестал его требовать.
395
- const unanswered = unansweredAxes(readAxes(assetsDir), config.variants);
436
+ const unanswered = unansweredAxes(readAxes(env.assetsDir), config.variants);
396
437
  if (unanswered.length) {
397
438
  return { code: 1, lines: ['раскладка не начата: не выбран вид', ...axisLines(unanswered)] };
398
439
  }
399
- // Незнакомое свойство опечатка, и молчать о ней нельзя ни с одной стороны. У дерева она
400
- // означает, что помеченного им ресурса оно не получит вовсе; у ресурса — что он не ляжет
401
- // никуда и никогда, а причину в имени файла не разглядеть.
402
- const strange = strangeTraits(config, assetsDir);
440
+ const strange = strangeTraits(config, env.assetsDir);
403
441
  if (strange.length) {
404
442
  return { code: 1, lines: ['раскладка не начата: свойство дерева не объявлено пакетом', ...strange] };
405
443
  }
406
- if (check) {
444
+ return null;
445
+ }
446
+ /** Ответ `sync --check`: расхождения считаются, на диск не пишется ничего. */
447
+ function syncCheck(config, root, version, assetsDir) {
448
+ {
407
449
  const result = planSync(config, root, version, assetsDir);
408
450
  const pending = pendingOf(result);
409
451
  // Незаполненный компаньон — такое же расхождение, как отставший файл: правило разложено,
@@ -414,12 +456,30 @@ export function sync(env, check) {
414
456
  // Разорванная связь в счёт расхождений не идёт: дерево вправе закрыть требование своим
415
457
  // средством, и отказ отбивал бы законную раскладку. Но и сходство её не отменяет —
416
458
  // предупреждение печатается и там, где расходиться больше нечему.
417
- const count = result.missing.size + result.gaps.length + pending.length + empty.length + result.unbound.length;
459
+ //
460
+ // Гард, подписанный не на то, что объявляет, идёт в счёт по той же причине и с большим
461
+ // основанием: неподключённый хотя бы не притворяется — этот выглядит работающим, и ветка
462
+ // его тела, ради которой всё писалось, не исполняется ни разу.
463
+ const count = result.missing.size + result.gaps.length + pending.length + empty.length + result.unbound.length + result.drifted.length;
418
464
  if (!count) {
419
465
  return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`, ...warnings(result)] };
420
466
  }
421
467
  return { code: 1, lines: [`sync --check: расхождений ${count}`, ...describe(result)] };
422
468
  }
469
+ }
470
+ export function sync(env, check) {
471
+ const { root, version, assetsDir } = env;
472
+ const config = readConfig(root);
473
+ if (!config) {
474
+ return { code: 1, lines: [NO_CONFIG] };
475
+ }
476
+ const refusal = refusalBeforeSync(env, config);
477
+ if (refusal) {
478
+ return refusal;
479
+ }
480
+ if (check) {
481
+ return syncCheck(config, root, version, assetsDir);
482
+ }
423
483
  const result = runSync(config, root, version, assetsDir);
424
484
  // Перечисляются только дырки: раскладка не начата, и назвать файл «положенным» значило бы
425
485
  // сказать неправду — на диске его нет.
@@ -456,6 +516,7 @@ export function sync(env, check) {
456
516
  ? [`разложено файлов: ${result.written.length}`, ...result.written.map((path) => ` ${path}`)]
457
517
  : ['всё уже разложено']),
458
518
  ...unboundLines(result),
519
+ ...driftedLines(result),
459
520
  ...debtLines(config, root, assetsDir),
460
521
  ...warnings(result),
461
522
  ],
@@ -524,6 +585,49 @@ const countLines = (counted) => {
524
585
  const width = counted.reduce((found, entry) => Math.max(found, entry.name.length), 0);
525
586
  return counted.map((entry) => ` ${entry.name.padEnd(width)} ${entry.count}`);
526
587
  };
588
+ /** Сводка строками: загруженное, незагруженное, отбивки гейта и отказы гардов. */
589
+ function statsLines(summary, swept, days, version, laidOut) {
590
+ return [
591
+ `наблюдения за ${days} дн., заходов ${summary.sessions}, событий ${summary.total}`,
592
+ ...(summary.versions.length ? [`версии пакета в записях: ${summary.versions.join(', ')}`] : []),
593
+ '',
594
+ `правил загружено: ${summary.loads.reduce((found, entry) => found + entry.count, 0)}`,
595
+ ...countLines(summary.loads),
596
+ ...(summary.unused.length
597
+ ? [
598
+ '',
599
+ `разложено и не загружено ни разу: ${summary.unused.length} из ${laidOut}`,
600
+ ...summary.unused.slice(0, UNUSED_SHOWN).map((name) => ` ${name}`),
601
+ // Список говорится не весь, и об этом говорится вслух: молчаливый обрыв
602
+ // читается как «вот они все», и правило, не попавшее в первую дюжину,
603
+ // считалось бы работающим.
604
+ ...(summary.unused.length > UNUSED_SHOWN
605
+ ? [` … и ещё ${summary.unused.length - UNUSED_SHOWN} — целиком в \`--json\``]
606
+ : []),
607
+ ]
608
+ : []),
609
+ ...(summary.denials.length
610
+ ? [
611
+ '',
612
+ `гейт отбивал: ${summary.denials.reduce((found, entry) => found + entry.count, 0)}`,
613
+ ...countLines(summary.denials),
614
+ ...(summary.kinds.length
615
+ ? [' чаще всего на:', ...countLines(summary.kinds).map((line) => ` ${line}`)]
616
+ : []),
617
+ ]
618
+ : []),
619
+ ...(summary.guards.length
620
+ ? [
621
+ '',
622
+ `гарды отказывали: ${summary.guards.reduce((found, entry) => found + entry.count, 0)}`,
623
+ ...countLines(summary.guards),
624
+ ]
625
+ : []),
626
+ ...(swept.length ? ['', `снято по сроку хранения (${KEEP_DAYS} дн.): ${swept.length}`] : []),
627
+ '',
628
+ `пакет v${version}`,
629
+ ];
630
+ }
527
631
  /**
528
632
  * Сводка наблюдений за отрезок дней.
529
633
  *
@@ -575,46 +679,7 @@ export function stats(env, options) {
575
679
  }
576
680
  return {
577
681
  code: 0,
578
- lines: [
579
- `наблюдения за ${days} дн., заходов ${summary.sessions}, событий ${summary.total}`,
580
- ...(summary.versions.length ? [`версии пакета в записях: ${summary.versions.join(', ')}`] : []),
581
- '',
582
- `правил загружено: ${summary.loads.reduce((found, entry) => found + entry.count, 0)}`,
583
- ...countLines(summary.loads),
584
- ...(summary.unused.length
585
- ? [
586
- '',
587
- `разложено и не загружено ни разу: ${summary.unused.length} из ${laidOutSkills(config, assetsDir).length}`,
588
- ...summary.unused.slice(0, UNUSED_SHOWN).map((name) => ` ${name}`),
589
- // Список говорится не весь, и об этом говорится вслух: молчаливый обрыв
590
- // читается как «вот они все», и правило, не попавшее в первую дюжину,
591
- // считалось бы работающим.
592
- ...(summary.unused.length > UNUSED_SHOWN
593
- ? [` … и ещё ${summary.unused.length - UNUSED_SHOWN} — целиком в \`--json\``]
594
- : []),
595
- ]
596
- : []),
597
- ...(summary.denials.length
598
- ? [
599
- '',
600
- `гейт отбивал: ${summary.denials.reduce((found, entry) => found + entry.count, 0)}`,
601
- ...countLines(summary.denials),
602
- ...(summary.kinds.length
603
- ? [' чаще всего на:', ...countLines(summary.kinds).map((line) => ` ${line}`)]
604
- : []),
605
- ]
606
- : []),
607
- ...(summary.guards.length
608
- ? [
609
- '',
610
- `гарды отказывали: ${summary.guards.reduce((found, entry) => found + entry.count, 0)}`,
611
- ...countLines(summary.guards),
612
- ]
613
- : []),
614
- ...(result.swept.length ? ['', `снято по сроку хранения (${KEEP_DAYS} дн.): ${result.swept.length}`] : []),
615
- '',
616
- `пакет v${version}`,
617
- ],
682
+ lines: statsLines(summary, result.swept, days, version, laidOutSkills(config, assetsDir).length),
618
683
  };
619
684
  }
620
685
  export function doctor(env) {
@@ -658,7 +723,10 @@ export function doctor(env) {
658
723
  `не выбрано: ${catalog.length - taken - skipped - other - cut.size - needsTrait}, пропущено: ${skipped}, другой вид: ${other}, снято каскадом: ${cut.size}, нужно свойство: ${needsTrait}`,
659
724
  ...unchosen.map((entry) => ` не выбран: ${entry.id}`),
660
725
  ...needing.map((entry) => ` нужно свойство «${entry.needs}»: ${entry.id}`),
661
- ...cuts.map((one) => ` снят каскадом: ${one.id} — вслед за ${one.parent}${one.parent === one.root ? '' : `, отвергнут ${one.root}`}`),
726
+ ...cuts.map((one) => {
727
+ const root = one.parent === one.root ? '' : ', отвергнут ' + one.root;
728
+ return ` снят каскадом: ${one.id} — вслед за ${one.parent}${root}`;
729
+ }),
662
730
  ...profileLines(root, assetsDir, config),
663
731
  `значений в конфиге: ${Object.keys(config.vars).length}`,
664
732
  ...chosen,