@rt-tools/agent-kit 0.8.1 → 0.8.3

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 (129) hide show
  1. package/README.md +25 -19
  2. package/assets/agents/qa-engineer.md +1 -1
  3. package/assets/agents/rules-reviewer.md +83 -0
  4. package/assets/checks/board.github.mjs +56 -17
  5. package/assets/checks/check-board.github.mjs +49 -5
  6. package/assets/checks/check-dupes.mjs +66 -6
  7. package/assets/checks/check-specs.mjs +100 -15
  8. package/assets/checks/rt-kit-checks.config.mjs +9 -0
  9. package/assets/checks/task-new.github.mjs +33 -5
  10. package/assets/commands/agent-kit-digest.md +10 -5
  11. package/assets/commands/feedback.md +95 -0
  12. package/assets/commands/next-session.md +4 -4
  13. package/assets/commands/rules-review.md +98 -0
  14. package/assets/commands/skill-curator.md +44 -25
  15. package/assets/defaults/gate-map.sh +11 -4
  16. package/assets/defaults/project.sh +46 -0
  17. package/assets/docs/GLOSSARY.md +49 -46
  18. package/assets/hooks/docs-guard.sh +19 -3
  19. package/assets/hooks/git-guard-delivery.sh +106 -13
  20. package/assets/hooks/proposal-guard.sh +93 -0
  21. package/assets/hooks/reuse-first-guard.sh +83 -15
  22. package/assets/hooks/skill-gate-layers.sh +1 -1
  23. package/assets/hooks/skill-gate.sh +27 -1
  24. package/assets/hooks/task-flow-guard.sh +59 -19
  25. package/assets/hooks/waiting-turn-guard.sh +87 -0
  26. package/assets/hooks/window-fill-guard.sh +1 -1
  27. package/assets/laws/delivery.md +41 -7
  28. package/assets/laws/project-documentation.md +39 -0
  29. package/assets/laws/verifiability.md +6 -1
  30. package/assets/laws/work-conduct.md +83 -3
  31. package/assets/patterns/git-workflow-commit.azure.md +84 -4
  32. package/assets/patterns/git-workflow-commit.github.md +90 -4
  33. package/assets/patterns/git-workflow-commit.gitlab.md +84 -5
  34. package/assets/patterns/git-workflow-docker.md +30 -0
  35. package/assets/patterns/git-workflow-merge.md +1 -1
  36. package/assets/patterns/spec-driven-domain.md +7 -1
  37. package/assets/patterns/spec-driven-rule.md +6 -0
  38. package/assets/patterns/task-flow-close.md +197 -21
  39. package/assets/patterns/task-flow-handoff.md +28 -5
  40. package/assets/patterns/task-flow-resume.md +25 -7
  41. package/assets/patterns/task-flow-start.md +37 -6
  42. package/assets/rules/angular-patterns.md +22 -0
  43. package/assets/rules/api-layer.md +25 -0
  44. package/assets/rules/browser-verification.md +42 -1
  45. package/assets/rules/component-structure.md +21 -0
  46. package/assets/rules/dependencies.md +22 -0
  47. package/assets/rules/doc-style.md +37 -5
  48. package/assets/rules/{entity-conventions.md → entity-conventions.needs-admin.md} +21 -0
  49. package/assets/rules/entity-models.md +21 -0
  50. package/assets/rules/git-workflow.azure.md +62 -8
  51. package/assets/rules/git-workflow.github.md +78 -14
  52. package/assets/rules/git-workflow.gitlab.md +61 -8
  53. package/assets/rules/lib-layers.md +25 -0
  54. package/assets/rules/lists.md +27 -0
  55. package/assets/rules/navigation.md +21 -0
  56. package/assets/rules/{observability.md → observability.needs-app.md} +23 -0
  57. package/assets/rules/permissions.md +23 -0
  58. package/assets/rules/platform-access.md +21 -0
  59. package/assets/rules/reuse-first.md +20 -0
  60. package/assets/rules/seo.md +19 -0
  61. package/assets/rules/shared-code.md +19 -0
  62. package/assets/rules/spec-driven.md +36 -0
  63. package/assets/rules/styling-bem.md +19 -0
  64. package/assets/rules/task-flow.md +150 -35
  65. package/assets/rules/testing.md +50 -0
  66. package/assets/rules/translations.md +21 -0
  67. package/assets/rules/typescript-conventions.md +33 -0
  68. package/assets/samples/specs/_template/spec.md +83 -0
  69. package/assets/samples/tasks/_template/grill.md +28 -0
  70. package/assets/samples/tasks/_template/plan.md +39 -0
  71. package/assets/samples/tasks/_template/progress.md +23 -0
  72. package/assets/skills/agent-kit-extend.md +24 -0
  73. package/assets/skills/agent-kit.md +69 -7
  74. package/assets/templates/rule.md +31 -2
  75. package/assets/traits.json +14 -0
  76. package/bin/agent-kit.d.ts.map +1 -1
  77. package/bin/agent-kit.js +31 -16
  78. package/bin/agent-kit.js.map +1 -1
  79. package/index.d.ts +1 -0
  80. package/index.d.ts.map +1 -1
  81. package/index.js +1 -0
  82. package/index.js.map +1 -1
  83. package/lib/argv.d.ts +17 -0
  84. package/lib/argv.d.ts.map +1 -0
  85. package/lib/argv.js +44 -0
  86. package/lib/argv.js.map +1 -0
  87. package/lib/cargo.d.ts +88 -0
  88. package/lib/cargo.d.ts.map +1 -0
  89. package/lib/cargo.js +16 -0
  90. package/lib/cargo.js.map +1 -0
  91. package/lib/catalog.d.ts +18 -1
  92. package/lib/catalog.d.ts.map +1 -1
  93. package/lib/catalog.js +12 -2
  94. package/lib/catalog.js.map +1 -1
  95. package/lib/commands.d.ts +0 -26
  96. package/lib/commands.d.ts.map +1 -1
  97. package/lib/commands.js +79 -122
  98. package/lib/commands.js.map +1 -1
  99. package/lib/companion.d.ts +37 -0
  100. package/lib/companion.d.ts.map +1 -1
  101. package/lib/companion.js +42 -1
  102. package/lib/companion.js.map +1 -1
  103. package/lib/config.d.ts +38 -1
  104. package/lib/config.d.ts.map +1 -1
  105. package/lib/config.js +24 -0
  106. package/lib/config.js.map +1 -1
  107. package/lib/ship.d.ts +39 -0
  108. package/lib/ship.d.ts.map +1 -0
  109. package/lib/ship.js +87 -0
  110. package/lib/ship.js.map +1 -0
  111. package/lib/shipment.d.ts +60 -0
  112. package/lib/shipment.d.ts.map +1 -0
  113. package/lib/shipment.js +247 -0
  114. package/lib/shipment.js.map +1 -0
  115. package/lib/snapshot.d.ts +30 -0
  116. package/lib/snapshot.d.ts.map +1 -0
  117. package/lib/snapshot.js +73 -0
  118. package/lib/snapshot.js.map +1 -0
  119. package/lib/traits.d.ts +32 -0
  120. package/lib/traits.d.ts.map +1 -0
  121. package/lib/traits.js +82 -0
  122. package/lib/traits.js.map +1 -0
  123. package/package.json +6 -2
  124. package/rt-tools-agent-kit-0.8.3.tgz +0 -0
  125. package/lib/submit.d.ts +0 -24
  126. package/lib/submit.d.ts.map +0 -1
  127. package/lib/submit.js +0 -26
  128. package/lib/submit.js.map +0 -1
  129. package/rt-tools-agent-kit-0.8.1.tgz +0 -0
package/lib/commands.js CHANGED
@@ -7,14 +7,14 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
7
7
  import { join } from 'node:path';
8
8
  import { collectAssets } from './assets.js';
9
9
  import { cascadeCuts, isChosen, readCatalog } from './catalog.js';
10
- import { isUnfilled } from './companion.js';
11
- import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, PROFILE_FILE, readConfig, RT_KIT_DIR, SKILL_KINDS, } from './config.js';
10
+ import { debtLine, isUnfilled, pathOf as companionPathOf, unaddressedOf } from './companion.js';
11
+ import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, PROFILE_FILE, readConfig, RT_KIT_DIR } from './config.js';
12
12
  import { hooksSection, SETTINGS_PATH } from './hooks-map.js';
13
13
  import { DEFAULT_DAYS, KEEP_DAYS, OBSERVATIONS_DIR, readObservations, summarize } from './observations.js';
14
14
  import { isRefusal } from './plan.js';
15
- import { leaksIn, markSent, marksOf, PROPOSALS_DIR, readProposals, TO_PACKAGE } from './proposals.js';
16
- import { FEEDBACK_LABEL } from './submit.js';
15
+ import { laidOutSkills } from './snapshot.js';
17
16
  import { pendingOf, planSync, runSync } from './sync.js';
17
+ import { answersRequirement, readTraits, unknownTraits } from './traits.js';
18
18
  import { placeholdersOf } from './vars.js';
19
19
  import { readAxes, unansweredAxes } from './variants.js';
20
20
  /** Что с файлом сделала раскладка. Прошедшее время здесь правда: `sync` уже записал. */
@@ -43,6 +43,8 @@ const CUT_BY_CASCADE = 'снят каскадом';
43
43
  const SKIPPED = 'пропущен';
44
44
  /** Ресурс чужого вида: в этом дереве его не существует, а не «от него отказались». */
45
45
  const OTHER_VARIANT = 'другой вид';
46
+ /** Ресурс, которому нужно свойство дерева: дерево его не отвергало — свойства у него нет. */
47
+ const NEEDS_TRAIT = 'нужно свойство';
46
48
  const KIND_TITLE = {
47
49
  laws: 'ЗАКОНЫ',
48
50
  rules: 'ПРАВИЛА',
@@ -56,6 +58,7 @@ const KIND_TITLE = {
56
58
  workflows: 'ВОРКФЛОУ',
57
59
  templates: 'ШАБЛОНЫ',
58
60
  docs: 'ДОКУМЕНТЫ',
61
+ samples: 'ОБРАЗЦЫ',
59
62
  };
60
63
  /** Состояние компаньона словами. У заполненного слова нет: о нём говорить нечего. */
61
64
  const COMPANION_WORD = {
@@ -135,6 +138,17 @@ function profileLines(root, assetsDir, config) {
135
138
  }
136
139
  const holes = (result) => [...result.missing].map(([asset, names]) => ` ${asset}: нет значений для ${names.map((name) => `{{${name}}}`).join(', ')}`);
137
140
  const unfilled = (result) => result.companions.filter(isUnfilled);
141
+ /**
142
+ * Строка про компаньон, которому нечего сказать.
143
+ *
144
+ * У правила, требующего свойства, которого дерево не назвало, это не черновик, а лишний файл:
145
+ * заполнять его нечем — ни одной статье такого правила здесь не отвечает ни файл, ни символ.
146
+ * Такое правило доезжает сюда только выбором поимённо, и верное действие тут обратное
147
+ * заполнению — снять ресурс строкой отказа.
148
+ */
149
+ const companionLine = (entry) => entry.needs === null
150
+ ? ` ${entry.path} — ${COMPANION_WORD[entry.state]}`
151
+ : ` ${entry.path} — правилу нужно свойство «${entry.needs}»: заполнять нечем, снимай ресурс строкой в \`skip\``;
138
152
  /** Ось без ответа: чем её спрашивают и из чего выбирают. */
139
153
  const axisLines = (axes) => axes.flatMap((axis) => [
140
154
  ` ${axis.name} — ${axis.question}`,
@@ -161,6 +175,23 @@ const gapLines = (result) => result.gaps.flatMap((gap) => [
161
175
  ` ${gap.kind}/${gap.name} — есть только под ${gap.axis}: ${gap.available.join(', ')}, а выбран «${gap.chosen}»`,
162
176
  ` либо заведи вид под «${gap.chosen}», либо назови в skip: ${gap.ids.join(', ')}`,
163
177
  ]);
178
+ /**
179
+ * Свойства, названные не по перечню пакета, — и деревом, и ресурсами.
180
+ *
181
+ * Обе стороны собираются одной функцией, потому что промах у них общий: имя свойства написано
182
+ * так, как его никто не объявлял. Разница только в том, где оно написано, и потому в строке
183
+ * отказа стоит место, а не одно имя.
184
+ */
185
+ function strangeTraits(config, assetsDir) {
186
+ const traits = readTraits(assetsDir);
187
+ const lines = unknownTraits(config.has, traits).map((trait) => ` \`${trait}\` — названо деревом в \`has\`, а пакет такого свойства не объявлял`);
188
+ for (const entry of readCatalog(assetsDir)) {
189
+ if (entry.needs !== null && unknownTraits([entry.needs], traits).length) {
190
+ lines.push(` \`${entry.needs}\` — требует ${entry.id}, а пакет такого свойства не объявлял`);
191
+ }
192
+ }
193
+ return lines.length ? [...lines, ` объявленные свойства: ${traits.map((trait) => trait.value).join(', ')}`] : [];
194
+ }
164
195
  /**
165
196
  * Гарды, которых нет в настройке агента, и готовый кусок для неё.
166
197
  *
@@ -265,7 +296,7 @@ const describe = (result) => [
265
296
  ...unboundLines(result),
266
297
  ...warnings(result),
267
298
  ...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
268
- ...unfilled(result).map((entry) => ` ${entry.path} — ${COMPANION_WORD[entry.state]}`),
299
+ ...unfilled(result).map(companionLine),
269
300
  ];
270
301
  /** Имена дырок во всех ресурсах, которые дерево берёт. Без конфига — ни одной: выбор неизвестен. */
271
302
  function placeholdersIn(config, assetsDir) {
@@ -317,6 +348,26 @@ export function init(root, only = [], variants = {}, assetsDir = '') {
317
348
  ],
318
349
  };
319
350
  }
351
+ /**
352
+ * Долг, добавленный этой раскладкой: статьи разложенных правил, у которых в компаньонах дерева
353
+ * нет адреса.
354
+ *
355
+ * Называется он здесь, а не отдельной сверкой, ровно потому, что через день его уже не отличить
356
+ * от накопленного: сверка видит расхождение, но не знает, чьим обновлением оно приехало.
357
+ */
358
+ function debtLines(config, root, assetsDir) {
359
+ const found = [];
360
+ for (const asset of collectAssets(config, assetsDir)) {
361
+ if (asset.kind !== 'rules') {
362
+ continue;
363
+ }
364
+ const path = join(root, companionPathOf(asset));
365
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
366
+ found.push(unaddressedOf(asset.name, asset.text, existing));
367
+ }
368
+ const line = debtLine(found);
369
+ return line === null ? [] : [line, 'адрес статье дописывается в компаньоне рядом с правилом'];
370
+ }
320
371
  /**
321
372
  * Отказ на устаревшей сборке. Раскладка из неё положила бы прежнюю редакцию ресурса и назвала
322
373
  * это сделанным: неправда дороже отказа — её замечают, когда правленое правило не действует.
@@ -346,6 +397,13 @@ export function sync(env, check) {
346
397
  if (unanswered.length) {
347
398
  return { code: 1, lines: ['раскладка не начата: не выбран вид', ...axisLines(unanswered)] };
348
399
  }
400
+ // Незнакомое свойство — опечатка, и молчать о ней нельзя ни с одной стороны. У дерева она
401
+ // означает, что помеченного им ресурса оно не получит вовсе; у ресурса — что он не ляжет
402
+ // никуда и никогда, а причину в имени файла не разглядеть.
403
+ const strange = strangeTraits(config, assetsDir);
404
+ if (strange.length) {
405
+ return { code: 1, lines: ['раскладка не начата: свойство дерева не объявлено пакетом', ...strange] };
406
+ }
349
407
  if (check) {
350
408
  const result = planSync(config, root, version, assetsDir);
351
409
  const pending = pendingOf(result);
@@ -399,6 +457,7 @@ export function sync(env, check) {
399
457
  ? [`разложено файлов: ${result.written.length}`, ...result.written.map((path) => ` ${path}`)]
400
458
  : ['всё уже разложено']),
401
459
  ...unboundLines(result),
460
+ ...debtLines(config, root, assetsDir),
402
461
  ...warnings(result),
403
462
  ],
404
463
  };
@@ -466,10 +525,6 @@ const countLines = (counted) => {
466
525
  const width = counted.reduce((found, entry) => Math.max(found, entry.name.length), 0);
467
526
  return counted.map((entry) => ` ${entry.name.padEnd(width)} ${entry.count}`);
468
527
  };
469
- /** Имена скилов, разложенных в это дерево: правила, паттерны и скилы без закона. */
470
- const skillsOf = (config, assetsDir) => collectAssets(config, assetsDir)
471
- .filter((asset) => SKILL_KINDS.includes(asset.kind))
472
- .map((asset) => asset.name);
473
528
  /**
474
529
  * Сводка наблюдений за отрезок дней.
475
530
  *
@@ -496,7 +551,7 @@ export function stats(env, options) {
496
551
  }
497
552
  const days = options.days > 0 ? options.days : DEFAULT_DAYS;
498
553
  const result = readObservations(root, options.today, days);
499
- const summary = summarize(result.observations, skillsOf(config, assetsDir), days);
554
+ const summary = summarize(result.observations, laidOutSkills(config, assetsDir), days);
500
555
  if (options.json) {
501
556
  return { code: 0, lines: [JSON.stringify({ ...summary, swept: result.swept })] };
502
557
  }
@@ -530,7 +585,7 @@ export function stats(env, options) {
530
585
  ...(summary.unused.length
531
586
  ? [
532
587
  '',
533
- `разложено и не загружено ни разу: ${summary.unused.length} из ${skillsOf(config, assetsDir).length}`,
588
+ `разложено и не загружено ни разу: ${summary.unused.length} из ${laidOutSkills(config, assetsDir).length}`,
534
589
  ...summary.unused.slice(0, UNUSED_SHOWN).map((name) => ` ${name}`),
535
590
  // Список говорится не весь, и об этом говорится вслух: молчаливый обрыв
536
591
  // читается как «вот они все», и правило, не попавшее в первую дюжину,
@@ -563,115 +618,6 @@ export function stats(env, options) {
563
618
  ],
564
619
  };
565
620
  }
566
- /** Тело записи: место, повод, готовый текст — и сводка под ними. */
567
- const issueBody = (proposal, summary, version) => [
568
- `Ресурс: \`${proposal.resource}\``,
569
- `Пакет: v${version}`,
570
- '',
571
- proposal.body,
572
- '',
573
- '## Наблюдения дерева, из которого пришло предложение',
574
- '',
575
- '```',
576
- ...summary,
577
- '```',
578
- '',
579
- 'Заведено `agent-kit propose`. Дерево, из которого оно пришло, здесь не называется намеренно.',
580
- ].join('\n');
581
- /**
582
- * Отправка предложений, адресованных пакету.
583
- *
584
- * Наружу уезжает только адрес «пакет»: «компаньон» и «дерево» — про имена и надстройки этого
585
- * дерева, и в репозитории пакета им делать нечего. Перед отправкой текст сверяется на адрес
586
- * дерева: файл уезжает в чужой репозиторий целиком, и проверка здесь дешевле, чем разбор потом.
587
- */
588
- export function propose(env, options) {
589
- const { root, version } = env;
590
- const config = readConfig(root);
591
- if (!config) {
592
- return { code: 1, lines: [NO_CONFIG] };
593
- }
594
- if (!options.repository) {
595
- return { code: 1, lines: ['куда отправлять — неизвестно: в манифесте пакета нет адреса репозитория'] };
596
- }
597
- const all = readProposals(root);
598
- const mine = all.filter((entry) => entry.address === TO_PACKAGE && !entry.sent);
599
- if (!mine.length) {
600
- const others = all.filter((entry) => entry.address !== TO_PACKAGE).length;
601
- const sent = all.filter((entry) => Boolean(entry.sent)).length;
602
- return {
603
- code: 0,
604
- lines: [
605
- 'отправлять нечего: предложений с адресом «пакет» и без пометки об отправке нет',
606
- ...(others ? [`с другими адресами: ${others} — они правятся здесь же, надстройкой и компаньоном`] : []),
607
- ...(sent ? [`уже отправлено: ${sent}`] : []),
608
- ...(all.length ? [] : [`предложения кладутся в ${PROPOSALS_DIR}/ — форма в шаблоне \`proposal.md\``]),
609
- ],
610
- };
611
- }
612
- // Утечка отбивает отправку целиком, а не свой блок: файл разбирает человек, и «уехало два из
613
- // трёх» он прочтёт как «всё в порядке».
614
- const marks = marksOf(root, options.remote);
615
- const leaked = mine.flatMap((proposal) => leaksIn(proposal.body, marks).map((leak) => ({ proposal, leak })));
616
- if (leaked.length) {
617
- return {
618
- code: 1,
619
- lines: [
620
- `отправка не начата: в тексте предложений назван адрес этого дерева — ${leaked.length}`,
621
- ...leaked.map(({ proposal, leak }) => ` ${proposal.file}:${proposal.line + leak.line} — ${leak.why}\n ${leak.text}`),
622
- 'предложение уезжает в чужой репозиторий целиком: правь текст, а не обходи проверку',
623
- ],
624
- };
625
- }
626
- if (options.dryRun) {
627
- return {
628
- code: 0,
629
- lines: [
630
- `уехало бы записей: ${mine.length} → ${options.repository}, метка ${FEEDBACK_LABEL}`,
631
- ...mine.map((entry) => ` ${entry.resource} (${entry.file}:${entry.line})`),
632
- 'сводка наблюдений едет вместе с ними',
633
- ],
634
- };
635
- }
636
- const done = [];
637
- for (const proposal of mine) {
638
- let url = '';
639
- try {
640
- url = options.submit({
641
- repository: options.repository,
642
- title: `предложение: ${proposal.resource}`,
643
- body: issueBody(proposal, options.summary, version),
644
- label: FEEDBACK_LABEL,
645
- });
646
- }
647
- catch (error) {
648
- return {
649
- code: 1,
650
- lines: [
651
- ...(done.length ? [`отправлено до отказа: ${done.length}`, ...done] : []),
652
- `отправка оборвалась на ${proposal.resource}: ${error.message.split('\n')[0]}`,
653
- // Три причины покрывают почти все отказы, и ни одну из них не видно из
654
- // сообщения помощника: он говорит про метку, доступ и сам себя одинаково глухо.
655
- `проверь: помощник хостинга стоит и вошёл в учётную запись; в ${options.repository} заведена метка ${FEEDBACK_LABEL}; у учётной записи есть право заводить записи`,
656
- 'файл предложений остался на месте — отправленное помечено, остальное уедет повторным запуском',
657
- ],
658
- };
659
- }
660
- // Пометка ложится сразу, а не после всех: оборвавшаяся отправка иначе увозит половину
661
- // предложений и не оставляет следа, по которому видно, какую именно.
662
- const path = join(root, proposal.file);
663
- writeFileSync(path, markSent(readFileSync(path, 'utf8'), proposal, url), 'utf8');
664
- done.push(` ${proposal.resource} → ${url}`);
665
- }
666
- return {
667
- code: 0,
668
- lines: [
669
- `заведено записей: ${done.length} в ${options.repository}`,
670
- ...done,
671
- 'разбирается сведением: `/agent-kit-digest` в репозитории пакета — оно и заводит задачу',
672
- ],
673
- };
674
- }
675
621
  export function doctor(env) {
676
622
  const { root, version, assetsDir } = env;
677
623
  const config = readConfig(root);
@@ -698,15 +644,21 @@ export function doctor(env) {
698
644
  // не отвергало — оно ушло вслед за родителем, и искать его в отказе читатель пойдёт зря.
699
645
  const cuts = cascadeCuts(catalog, config);
700
646
  const cut = new Set(cuts.map((one) => one.id));
647
+ // Ресурс с неотвеченным требованием в «не выбрано» не идёт по той же причине, что и чужой
648
+ // вид: дерево его не отвергало — свойства, без которого он бессмыслен, у него просто нет.
649
+ const needing = catalog.filter((entry) => entry.needs !== null && !answersRequirement(entry.needs, config.has) && !config.skip.includes(entry.id));
650
+ const needsTrait = needing.length;
701
651
  // Невыбранное называется поимённо: число «не выбрано: 73» не отвечает ни на один вопрос,
702
652
  // ради которого его читают, — ни какого ресурса не хватает, ни требуется ли он соседу.
703
653
  const unchosen = catalog.filter((entry) => !isChosen(entry, config) &&
704
654
  !config.skip.includes(entry.id) &&
705
- foreignVariant.every((one) => one.id !== entry.id));
655
+ foreignVariant.every((one) => one.id !== entry.id) &&
656
+ needing.every((one) => one.id !== entry.id));
706
657
  const lines = [
707
658
  `пакет v${version}, везёт ресурсов ${catalog.length}, взято ${taken}`,
708
- `не выбрано: ${catalog.length - taken - skipped - other - cut.size}, пропущено: ${skipped}, другой вид: ${other}, снято каскадом: ${cut.size}`,
659
+ `не выбрано: ${catalog.length - taken - skipped - other - cut.size - needsTrait}, пропущено: ${skipped}, другой вид: ${other}, снято каскадом: ${cut.size}, нужно свойство: ${needsTrait}`,
709
660
  ...unchosen.map((entry) => ` не выбран: ${entry.id}`),
661
+ ...needing.map((entry) => ` нужно свойство «${entry.needs}»: ${entry.id}`),
710
662
  ...cuts.map((one) => ` снят каскадом: ${one.id} — вслед за ${one.parent}${one.parent === one.root ? '' : `, отвергнут ${one.root}`}`),
711
663
  ...profileLines(root, assetsDir, config),
712
664
  `значений в конфиге: ${Object.keys(config.vars).length}`,
@@ -747,6 +699,11 @@ export function list(env) {
747
699
  if (config.skip.includes(entry.id)) {
748
700
  return SKIPPED;
749
701
  }
702
+ // Неотвеченное требование — не отказ дерева: оно этого ресурса не выбирало и не
703
+ // отвергало, а свойства, без которого ресурс бессмыслен, у него просто нет.
704
+ if (entry.needs !== null && !answersRequirement(entry.needs, config.has)) {
705
+ return `${NEEDS_TRAIT}: ${entry.needs}`;
706
+ }
750
707
  if (!isChosen(entry, config)) {
751
708
  return NOT_CHOSEN;
752
709
  }