@rt-tools/agent-kit 0.8.3 → 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 (132) hide show
  1. package/README.md +12 -0
  2. package/assets/checks/board.github.mjs +48 -1
  3. package/assets/checks/check-board.github.mjs +84 -1
  4. package/assets/checks/check-lib-layers.mjs +13 -524
  5. package/assets/checks/check-specs.mjs +11 -782
  6. package/assets/checks/check-styles.mjs +185 -15
  7. package/assets/checks/lib-boundaries.mjs +143 -0
  8. package/assets/checks/lib-common.mjs +149 -0
  9. package/assets/checks/lib-domains.mjs +205 -0
  10. package/assets/checks/lib-manifests.mjs +60 -0
  11. package/assets/checks/lib-reexports.mjs +101 -0
  12. package/assets/checks/rt-kit-checks.config.mjs +17 -0
  13. package/assets/checks/spec-anchors.mjs +297 -0
  14. package/assets/checks/spec-common.mjs +222 -0
  15. package/assets/checks/spec-contract.mjs +152 -0
  16. package/assets/checks/spec-scenarios.mjs +201 -0
  17. package/assets/defaults/project.sh +8 -0
  18. package/assets/hooks/git-guard-push-tests.sh +8 -4
  19. package/assets/hooks/skill-gate.sh +1 -1
  20. package/assets/hooks/sql-guard-parse.sh +187 -0
  21. package/assets/hooks/sql-guard-request.sh +117 -0
  22. package/assets/hooks/sql-guard-target.sh +134 -0
  23. package/assets/hooks/sql-guard-write.sh +212 -0
  24. package/assets/hooks/sql-guard.sh +26 -596
  25. package/assets/hooks/waiting-turn-guard.sh +42 -13
  26. package/assets/laws/delivery.md +7 -0
  27. package/assets/laws/work-conduct.md +9 -0
  28. package/assets/patterns/admin-lists-screen.md +25 -14
  29. package/assets/patterns/admin-nav-item.md +1 -1
  30. package/assets/patterns/component-structure-new.md +1 -1
  31. package/assets/patterns/entity-aside.md +4 -2
  32. package/assets/patterns/observability-record.md +9 -0
  33. package/assets/patterns/shared-code-new.md +2 -2
  34. package/assets/patterns/task-flow-close.md +7 -1
  35. package/assets/rules/git-workflow.azure.md +7 -0
  36. package/assets/rules/git-workflow.github.md +41 -0
  37. package/assets/rules/git-workflow.gitlab.md +7 -0
  38. package/assets/rules/lib-layers.md +4 -0
  39. package/assets/rules/lists.md +10 -10
  40. package/assets/rules/shared-code.md +1 -1
  41. package/assets/rules/task-flow.md +47 -7
  42. package/assets/rules/testing.md +31 -0
  43. package/assets/rules/typescript-conventions.md +7 -0
  44. package/assets/skills/agent-kit.md +36 -0
  45. package/assets/templates/proposal.md +21 -0
  46. package/bin/agent-kit.d.ts.map +1 -1
  47. package/bin/agent-kit.js +115 -87
  48. package/bin/agent-kit.js.map +1 -1
  49. package/index.d.ts +1 -0
  50. package/index.d.ts.map +1 -1
  51. package/index.js +1 -0
  52. package/index.js.map +1 -1
  53. package/lib/argv.d.ts.map +1 -1
  54. package/lib/argv.js +6 -4
  55. package/lib/argv.js.map +1 -1
  56. package/lib/assets.d.ts.map +1 -1
  57. package/lib/assets.js +2 -1
  58. package/lib/assets.js.map +1 -1
  59. package/lib/cargo.d.ts +20 -0
  60. package/lib/cargo.d.ts.map +1 -1
  61. package/lib/cargo.js.map +1 -1
  62. package/lib/cascade.d.ts +55 -0
  63. package/lib/cascade.d.ts.map +1 -0
  64. package/lib/cascade.js +131 -0
  65. package/lib/cascade.js.map +1 -0
  66. package/lib/catalog.d.ts +0 -75
  67. package/lib/catalog.d.ts.map +1 -1
  68. package/lib/catalog.js +44 -127
  69. package/lib/catalog.js.map +1 -1
  70. package/lib/commands.d.ts.map +1 -1
  71. package/lib/commands.js +152 -85
  72. package/lib/commands.js.map +1 -1
  73. package/lib/companion.d.ts.map +1 -1
  74. package/lib/companion.js +5 -5
  75. package/lib/companion.js.map +1 -1
  76. package/lib/config.d.ts +2 -0
  77. package/lib/config.d.ts.map +1 -1
  78. package/lib/config.js +7 -5
  79. package/lib/config.js.map +1 -1
  80. package/lib/enroll.d.ts +56 -0
  81. package/lib/enroll.d.ts.map +1 -0
  82. package/lib/enroll.js +123 -0
  83. package/lib/enroll.js.map +1 -0
  84. package/lib/freshness.d.ts.map +1 -1
  85. package/lib/freshness.js +31 -17
  86. package/lib/freshness.js.map +1 -1
  87. package/lib/hooks-map.d.ts +30 -0
  88. package/lib/hooks-map.d.ts.map +1 -1
  89. package/lib/hooks-map.js +80 -18
  90. package/lib/hooks-map.js.map +1 -1
  91. package/lib/integrity.d.ts +1 -2
  92. package/lib/integrity.d.ts.map +1 -1
  93. package/lib/integrity.js +0 -1
  94. package/lib/integrity.js.map +1 -1
  95. package/lib/observations.d.ts.map +1 -1
  96. package/lib/observations.js +25 -12
  97. package/lib/observations.js.map +1 -1
  98. package/lib/order.d.ts +10 -0
  99. package/lib/order.d.ts.map +1 -0
  100. package/lib/order.js +14 -0
  101. package/lib/order.js.map +1 -0
  102. package/lib/picker.d.ts.map +1 -1
  103. package/lib/picker.js +8 -2
  104. package/lib/picker.js.map +1 -1
  105. package/lib/plan.js +1 -1
  106. package/lib/plan.js.map +1 -1
  107. package/lib/proposals.d.ts.map +1 -1
  108. package/lib/proposals.js +25 -8
  109. package/lib/proposals.js.map +1 -1
  110. package/lib/sections.js +1 -1
  111. package/lib/sections.js.map +1 -1
  112. package/lib/ship.d.ts.map +1 -1
  113. package/lib/ship.js +9 -1
  114. package/lib/ship.js.map +1 -1
  115. package/lib/shipment.d.ts.map +1 -1
  116. package/lib/shipment.js +14 -10
  117. package/lib/shipment.js.map +1 -1
  118. package/lib/snapshot.d.ts.map +1 -1
  119. package/lib/snapshot.js +2 -1
  120. package/lib/snapshot.js.map +1 -1
  121. package/lib/stamp.js +1 -1
  122. package/lib/stamp.js.map +1 -1
  123. package/lib/sync.d.ts +12 -2
  124. package/lib/sync.d.ts.map +1 -1
  125. package/lib/sync.js +11 -10
  126. package/lib/sync.js.map +1 -1
  127. package/lib/vars.d.ts.map +1 -1
  128. package/lib/vars.js +2 -3
  129. package/lib/vars.js.map +1 -1
  130. package/package.json +1 -1
  131. package/rt-tools-agent-kit-0.9.0.tgz +0 -0
  132. package/rt-tools-agent-kit-0.8.3.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: 'положен',
@@ -83,20 +86,9 @@ const PROFILE_HELPER = 'rt_needs';
83
86
  * есть.
84
87
  */
85
88
  const PROFILE_DEFINE = /(rt_[a-z_]+)\s*\(\)\s*\{/g;
86
- /**
87
- * Функции профиля, которых ждут взятые хуки, и те из них, что дерево не определило.
88
- *
89
- * Отдельной строкой в разборе состояния потому, что на месте вызова нехватка не видна вовсе:
90
- * хук выходит с нулём, стоит в настройке, виден в списке — и читается как работающий. Тишина
91
- * при этом означает разом «нечего проверять», «проверять нечем» и «всё в порядке».
92
- */
93
- function profileLines(root, assetsDir, config) {
94
- const wanted = new Set();
95
- const defined = new Set();
96
- for (const asset of collectAssets(config, assetsDir)) {
97
- if (asset.kind !== 'hooks') {
98
- continue;
99
- }
89
+ /** Что взятые хуки зовут у профиля и что определяют сами. */
90
+ function readHooks(assets, wanted, defined) {
91
+ for (const asset of assets.filter((one) => one.kind === 'hooks')) {
100
92
  for (const found of asset.text.matchAll(PROFILE_CALL)) {
101
93
  if (found[1] !== PROFILE_HELPER) {
102
94
  wanted.add(found[1]);
@@ -109,23 +101,36 @@ function profileLines(root, assetsDir, config) {
109
101
  defined.add(found[1]);
110
102
  }
111
103
  }
112
- if (!wanted.size) {
113
- return [];
114
- }
104
+ }
105
+ /** Функции, определённые самим профилем: разложенным умолчанием пакета и надстройкой дерева. */
106
+ function readProfiles(root, config, defined) {
115
107
  // Профиль собирается из тех же файлов, что читает сам хук: сперва разложенное умолчание
116
108
  // пакета, поверх — надстройка дерева. Судить по одной надстройке значило бы объявить мёртвым
117
109
  // каждый гард дерева, которое умолчаний не переписывало.
118
110
  const defaults = config.layout.defaults ?? DEFAULT_LAYOUT.defaults;
119
111
  const sources = [join(root, defaults, PROFILE_FILE), join(root, RT_KIT_DIR, PROFILE_FILE)];
120
- for (const path of sources) {
121
- if (!existsSync(path)) {
122
- continue;
123
- }
112
+ for (const path of sources.filter((one) => existsSync(one))) {
124
113
  for (const found of readFileSync(path, 'utf8').matchAll(PROFILE_DEFINE)) {
125
114
  defined.add(found[1]);
126
115
  }
127
116
  }
128
- 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);
129
134
  return [
130
135
  `функции профиля, которых ждут взятые хуки: ${wanted.size}`,
131
136
  ...(absent.length
@@ -136,7 +141,14 @@ function profileLines(root, assetsDir, config) {
136
141
  : [' все определены']),
137
142
  ];
138
143
  }
139
- 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
+ });
140
152
  const unfilled = (result) => result.companions.filter(isUnfilled);
141
153
  /**
142
154
  * Строка про компаньон, которому нечего сказать.
@@ -201,13 +213,34 @@ function strangeTraits(config, assetsDir) {
201
213
  const unboundLines = (result) => result.unbound.length
202
214
  ? [
203
215
  `гарды разложены, но в \`${SETTINGS_PATH}\` их не зовёт никто: ${result.unbound.length}`,
204
- ...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
+ }),
205
220
  ` вставь в \`${SETTINGS_PATH}\` раздел \`hooks\` — готовый кусок ниже:`,
206
221
  ...JSON.stringify({ hooks: hooksSection(result.unbound) }, null, 4)
207
222
  .split('\n')
208
223
  .map((line) => ` ${line}`),
209
224
  ]
210
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
+ : [];
211
244
  /**
212
245
  * Разорванные связи между ресурсами.
213
246
  *
@@ -231,7 +264,10 @@ const brokenLines = (result) => result.broken.length
231
264
  const idleLines = (result) => result.idle.length
232
265
  ? [
233
266
  `строк отказа, которые ничего не снимают: ${result.idle.length}`,
234
- ...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
+ }),
235
271
  ' это предупреждение, а не отказ: строки убирает дерево, и раскладка идёт дальше',
236
272
  ]
237
273
  : [];
@@ -294,6 +330,7 @@ const describe = (result) => [
294
330
  ...holes(result),
295
331
  ...gapLines(result),
296
332
  ...unboundLines(result),
333
+ ...driftedLines(result),
297
334
  ...warnings(result),
298
335
  ...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
299
336
  ...unfilled(result).map(companionLine),
@@ -311,7 +348,7 @@ function placeholdersIn(config, assetsDir) {
311
348
  }
312
349
  }
313
350
  }
314
- return names.sort();
351
+ return names.sort(byText);
315
352
  }
316
353
  /**
317
354
  * Что проект выбрал при заведении конфига. Пустой список — весь набор; выбор приходит сюда
@@ -323,7 +360,7 @@ export function init(root, only = [], variants = {}, assetsDir = '') {
323
360
  if (existsSync(path)) {
324
361
  return { code: 0, lines: [`${CONFIG_PATH} уже есть — оставлен как есть`] };
325
362
  }
326
- const config = { vars: {}, layout: DEFAULT_LAYOUT, variants, only, skip: [] };
363
+ const config = { vars: {}, layout: DEFAULT_LAYOUT, skip: [], variants, only };
327
364
  mkdirSync(join(root, OVERRIDES_DIR), { recursive: true });
328
365
  mkdirSync(join(root, CONFIG_PATH, '..'), { recursive: true });
329
366
  writeFileSync(path, `${JSON.stringify(config, null, 4)}\n`, 'utf8');
@@ -381,30 +418,34 @@ const staleRefusal = (stale) => ({
381
418
  ' собери пакет и повтори',
382
419
  ],
383
420
  });
384
- export function sync(env, check) {
385
- const { root, version, assetsDir } = env;
386
- const config = readConfig(root);
387
- if (!config) {
388
- return { code: 1, lines: [NO_CONFIG] };
389
- }
421
+ /**
422
+ * Отказ, случившийся до первой записи на диск. Пусто — раскладке ничто не мешает.
423
+ *
424
+ * Ось без ответа отбивает раскладку целиком, а не пропускает свои ресурсы молча: правило
425
+ * поставки бывает в трёх видах, и дерево, не назвавшее свой, осталось бы вовсе без правила
426
+ * поставки — заметить это можно было бы только по тому, что гейт перестал его требовать.
427
+ *
428
+ * Незнакомое свойство — опечатка, и молчать о ней нельзя ни с одной стороны. У дерева она
429
+ * означает, что помеченного им ресурса оно не получит вовсе; у ресурса — что он не ляжет
430
+ * никуда и никогда, а причину в имени файла не разглядеть.
431
+ */
432
+ function refusalBeforeSync(env, config) {
390
433
  if (env.stale) {
391
434
  return staleRefusal(env.stale);
392
435
  }
393
- // Ось без ответа отбивает раскладку целиком, а не пропускает свои ресурсы молча: правило
394
- // поставки бывает в трёх видах, и дерево, не назвавшее свой, осталось бы вовсе без правила
395
- // поставки — заметить это можно было бы только по тому, что гейт перестал его требовать.
396
- const unanswered = unansweredAxes(readAxes(assetsDir), config.variants);
436
+ const unanswered = unansweredAxes(readAxes(env.assetsDir), config.variants);
397
437
  if (unanswered.length) {
398
438
  return { code: 1, lines: ['раскладка не начата: не выбран вид', ...axisLines(unanswered)] };
399
439
  }
400
- // Незнакомое свойство опечатка, и молчать о ней нельзя ни с одной стороны. У дерева она
401
- // означает, что помеченного им ресурса оно не получит вовсе; у ресурса — что он не ляжет
402
- // никуда и никогда, а причину в имени файла не разглядеть.
403
- const strange = strangeTraits(config, assetsDir);
440
+ const strange = strangeTraits(config, env.assetsDir);
404
441
  if (strange.length) {
405
442
  return { code: 1, lines: ['раскладка не начата: свойство дерева не объявлено пакетом', ...strange] };
406
443
  }
407
- if (check) {
444
+ return null;
445
+ }
446
+ /** Ответ `sync --check`: расхождения считаются, на диск не пишется ничего. */
447
+ function syncCheck(config, root, version, assetsDir) {
448
+ {
408
449
  const result = planSync(config, root, version, assetsDir);
409
450
  const pending = pendingOf(result);
410
451
  // Незаполненный компаньон — такое же расхождение, как отставший файл: правило разложено,
@@ -415,12 +456,30 @@ export function sync(env, check) {
415
456
  // Разорванная связь в счёт расхождений не идёт: дерево вправе закрыть требование своим
416
457
  // средством, и отказ отбивал бы законную раскладку. Но и сходство её не отменяет —
417
458
  // предупреждение печатается и там, где расходиться больше нечему.
418
- 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;
419
464
  if (!count) {
420
465
  return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`, ...warnings(result)] };
421
466
  }
422
467
  return { code: 1, lines: [`sync --check: расхождений ${count}`, ...describe(result)] };
423
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
+ }
424
483
  const result = runSync(config, root, version, assetsDir);
425
484
  // Перечисляются только дырки: раскладка не начата, и назвать файл «положенным» значило бы
426
485
  // сказать неправду — на диске его нет.
@@ -457,6 +516,7 @@ export function sync(env, check) {
457
516
  ? [`разложено файлов: ${result.written.length}`, ...result.written.map((path) => ` ${path}`)]
458
517
  : ['всё уже разложено']),
459
518
  ...unboundLines(result),
519
+ ...driftedLines(result),
460
520
  ...debtLines(config, root, assetsDir),
461
521
  ...warnings(result),
462
522
  ],
@@ -525,6 +585,49 @@ const countLines = (counted) => {
525
585
  const width = counted.reduce((found, entry) => Math.max(found, entry.name.length), 0);
526
586
  return counted.map((entry) => ` ${entry.name.padEnd(width)} ${entry.count}`);
527
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
+ }
528
631
  /**
529
632
  * Сводка наблюдений за отрезок дней.
530
633
  *
@@ -576,46 +679,7 @@ export function stats(env, options) {
576
679
  }
577
680
  return {
578
681
  code: 0,
579
- lines: [
580
- `наблюдения за ${days} дн., заходов ${summary.sessions}, событий ${summary.total}`,
581
- ...(summary.versions.length ? [`версии пакета в записях: ${summary.versions.join(', ')}`] : []),
582
- '',
583
- `правил загружено: ${summary.loads.reduce((found, entry) => found + entry.count, 0)}`,
584
- ...countLines(summary.loads),
585
- ...(summary.unused.length
586
- ? [
587
- '',
588
- `разложено и не загружено ни разу: ${summary.unused.length} из ${laidOutSkills(config, assetsDir).length}`,
589
- ...summary.unused.slice(0, UNUSED_SHOWN).map((name) => ` ${name}`),
590
- // Список говорится не весь, и об этом говорится вслух: молчаливый обрыв
591
- // читается как «вот они все», и правило, не попавшее в первую дюжину,
592
- // считалось бы работающим.
593
- ...(summary.unused.length > UNUSED_SHOWN
594
- ? [` … и ещё ${summary.unused.length - UNUSED_SHOWN} — целиком в \`--json\``]
595
- : []),
596
- ]
597
- : []),
598
- ...(summary.denials.length
599
- ? [
600
- '',
601
- `гейт отбивал: ${summary.denials.reduce((found, entry) => found + entry.count, 0)}`,
602
- ...countLines(summary.denials),
603
- ...(summary.kinds.length
604
- ? [' чаще всего на:', ...countLines(summary.kinds).map((line) => ` ${line}`)]
605
- : []),
606
- ]
607
- : []),
608
- ...(summary.guards.length
609
- ? [
610
- '',
611
- `гарды отказывали: ${summary.guards.reduce((found, entry) => found + entry.count, 0)}`,
612
- ...countLines(summary.guards),
613
- ]
614
- : []),
615
- ...(result.swept.length ? ['', `снято по сроку хранения (${KEEP_DAYS} дн.): ${result.swept.length}`] : []),
616
- '',
617
- `пакет v${version}`,
618
- ],
682
+ lines: statsLines(summary, result.swept, days, version, laidOutSkills(config, assetsDir).length),
619
683
  };
620
684
  }
621
685
  export function doctor(env) {
@@ -659,7 +723,10 @@ export function doctor(env) {
659
723
  `не выбрано: ${catalog.length - taken - skipped - other - cut.size - needsTrait}, пропущено: ${skipped}, другой вид: ${other}, снято каскадом: ${cut.size}, нужно свойство: ${needsTrait}`,
660
724
  ...unchosen.map((entry) => ` не выбран: ${entry.id}`),
661
725
  ...needing.map((entry) => ` нужно свойство «${entry.needs}»: ${entry.id}`),
662
- ...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
+ }),
663
730
  ...profileLines(root, assetsDir, config),
664
731
  `значений в конфиге: ${Object.keys(config.vars).length}`,
665
732
  ...chosen,