@rt-tools/agent-kit 0.5.1 → 0.5.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.
- package/README.md +102 -6
- package/assets/commands/agent-kit-digest.md +83 -0
- package/assets/commands/skill-curator.md +33 -1
- package/assets/defaults/gate-map.sh +2 -0
- package/assets/defaults/project.sh +12 -1
- package/assets/docs/GLOSSARY.md +3 -0
- package/assets/hooks/docs-guard.sh +18 -1
- package/assets/hooks/git-guard-delivery.sh +30 -3
- package/assets/hooks/git-guard-main.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -1
- package/assets/hooks/grill-gate.sh +38 -16
- package/assets/hooks/lint-after-edit.sh +10 -3
- package/assets/hooks/observe.sh +90 -0
- package/assets/hooks/postmortem-guard.sh +92 -0
- package/assets/hooks/profile-check.sh +43 -0
- package/assets/hooks/qa-dataid-guard.sh +9 -2
- package/assets/hooks/reuse-first-guard.sh +9 -2
- package/assets/hooks/skill-gate.sh +15 -0
- package/assets/hooks/skill-loaded.sh +7 -0
- package/assets/hooks/task-context-load.sh +16 -2
- package/assets/hooks/task-flow-guard.sh +9 -2
- package/assets/hooks/window-fill-guard.sh +8 -1
- package/assets/laws/project-documentation.md +5 -0
- package/assets/laws/verifiability.md +9 -0
- package/assets/laws/work-conduct.md +39 -0
- package/assets/patterns/git-workflow-commit.azure.md +16 -12
- package/assets/patterns/git-workflow-commit.github.md +16 -12
- package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
- package/assets/patterns/task-flow-resume.md +12 -0
- package/assets/patterns/task-flow-start.md +36 -2
- package/assets/rules/git-workflow.azure.md +15 -0
- package/assets/rules/git-workflow.github.md +15 -0
- package/assets/rules/git-workflow.gitlab.md +15 -0
- package/assets/rules/spec-driven.md +6 -0
- package/assets/rules/task-flow.md +24 -3
- package/assets/skills/agent-kit.md +32 -0
- package/assets/templates/postmortem.md +32 -0
- package/assets/templates/proposal.md +39 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +50 -1
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts +33 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +55 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +41 -0
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +314 -3
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +8 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +4 -0
- package/lib/config.js.map +1 -1
- package/lib/observations.d.ts +72 -0
- package/lib/observations.d.ts.map +1 -0
- package/lib/observations.js +126 -0
- package/lib/observations.js.map +1 -0
- package/lib/proposals.d.ts +48 -0
- package/lib/proposals.d.ts.map +1 -0
- package/lib/proposals.js +111 -0
- package/lib/proposals.js.map +1 -0
- package/lib/submit.d.ts +24 -0
- package/lib/submit.d.ts.map +1 -0
- package/lib/submit.js +26 -0
- package/lib/submit.js.map +1 -0
- package/lib/sync.d.ts +9 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +2 -1
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.3.tgz +0 -0
- package/rt-tools-agent-kit-0.5.1.tgz +0 -0
package/lib/commands.js
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
* а `process.exit` зовёт только точка входа — иначе ни одну из них нельзя было бы проверить
|
|
4
4
|
* спекой, не перехватывая поток вывода.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { collectAssets } from './assets.js';
|
|
9
9
|
import { isChosen, readCatalog } from './catalog.js';
|
|
10
10
|
import { isUnfilled } from './companion.js';
|
|
11
|
-
import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, readConfig } from './config.js';
|
|
11
|
+
import { CONFIG_PATH, DEFAULT_LAYOUT, KINDS, OVERRIDES_DIR, PROFILE_FILE, readConfig, RT_KIT_DIR, SKILL_KINDS, } from './config.js';
|
|
12
12
|
import { hooksSection, SETTINGS_PATH } from './hooks-map.js';
|
|
13
|
+
import { DEFAULT_DAYS, KEEP_DAYS, OBSERVATIONS_DIR, readObservations, summarize } from './observations.js';
|
|
13
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';
|
|
14
17
|
import { pendingOf, planSync, runSync } from './sync.js';
|
|
15
18
|
import { placeholdersOf } from './vars.js';
|
|
16
19
|
import { readAxes, unansweredAxes } from './variants.js';
|
|
@@ -61,6 +64,73 @@ const COMPANION_WORD = {
|
|
|
61
64
|
const NO_CONFIG = `нет \`${CONFIG_PATH}\` — начни с \`agent-kit init\``;
|
|
62
65
|
/** Куда уезжает прежнее содержимое файла, отданного пакету. */
|
|
63
66
|
export const KEPT_SUFFIX = '.before-rt-kit';
|
|
67
|
+
/**
|
|
68
|
+
* Имя функции профиля, которую хук ждёт. Обе формы: голая проверка наличия и спрос через
|
|
69
|
+
* помощника, который о нехватке говорит вслух. Считать одну значило бы недосчитаться ровно тех
|
|
70
|
+
* хуков, которые перешли на второе.
|
|
71
|
+
*/
|
|
72
|
+
const PROFILE_CALL = /(?:command -v|rt_needs)[ \t]+(rt_[a-z_]+)/g;
|
|
73
|
+
/** Сам помощник функцией профиля не является: его везёт пакет, а не дерево. */
|
|
74
|
+
const PROFILE_HELPER = 'rt_needs';
|
|
75
|
+
/**
|
|
76
|
+
* Объявление функции: `rt_is_app_code() {`. Не с начала строки — хук объявляет запасной вариант
|
|
77
|
+
* прямо в условии, и не считать его объявлением значило бы звать недостающим то, что у хука
|
|
78
|
+
* есть.
|
|
79
|
+
*/
|
|
80
|
+
const PROFILE_DEFINE = /(rt_[a-z_]+)\s*\(\)\s*\{/g;
|
|
81
|
+
/**
|
|
82
|
+
* Функции профиля, которых ждут взятые хуки, и те из них, что дерево не определило.
|
|
83
|
+
*
|
|
84
|
+
* Отдельной строкой в разборе состояния потому, что на месте вызова нехватка не видна вовсе:
|
|
85
|
+
* хук выходит с нулём, стоит в настройке, виден в списке — и читается как работающий. Тишина
|
|
86
|
+
* при этом означает разом «нечего проверять», «проверять нечем» и «всё в порядке».
|
|
87
|
+
*/
|
|
88
|
+
function profileLines(root, assetsDir, config) {
|
|
89
|
+
const wanted = new Set();
|
|
90
|
+
const defined = new Set();
|
|
91
|
+
for (const asset of collectAssets(config, assetsDir)) {
|
|
92
|
+
if (asset.kind !== 'hooks') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const found of asset.text.matchAll(PROFILE_CALL)) {
|
|
96
|
+
if (found[1] !== PROFILE_HELPER) {
|
|
97
|
+
wanted.add(found[1]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Часть общих функций живёт не в профиле, а в соседнем хуке: запись наблюдения объявлена
|
|
101
|
+
// в хуке наблюдений, и гарды зовут её через ту же проверку наличия. Не считать их
|
|
102
|
+
// определёнными значило бы требовать от дерева переписать в профиль чужой код.
|
|
103
|
+
for (const found of asset.text.matchAll(PROFILE_DEFINE)) {
|
|
104
|
+
defined.add(found[1]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!wanted.size) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
// Профиль собирается из тех же файлов, что читает сам хук: сперва разложенное умолчание
|
|
111
|
+
// пакета, поверх — надстройка дерева. Судить по одной надстройке значило бы объявить мёртвым
|
|
112
|
+
// каждый гард дерева, которое умолчаний не переписывало.
|
|
113
|
+
const defaults = config.layout.defaults ?? DEFAULT_LAYOUT.defaults;
|
|
114
|
+
const sources = [join(root, defaults, PROFILE_FILE), join(root, RT_KIT_DIR, PROFILE_FILE)];
|
|
115
|
+
for (const path of sources) {
|
|
116
|
+
if (!existsSync(path)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const found of readFileSync(path, 'utf8').matchAll(PROFILE_DEFINE)) {
|
|
120
|
+
defined.add(found[1]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const absent = [...wanted].filter((name) => !defined.has(name)).sort();
|
|
124
|
+
return [
|
|
125
|
+
`функции профиля, которых ждут взятые хуки: ${wanted.size}`,
|
|
126
|
+
...(absent.length
|
|
127
|
+
? [
|
|
128
|
+
...absent.map((name) => ` нет функции ${name} — её определяют в ${join(RT_KIT_DIR, PROFILE_FILE)}`),
|
|
129
|
+
' хук без своей функции проверку не делает и действие пропускает',
|
|
130
|
+
]
|
|
131
|
+
: [' все определены']),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
64
134
|
const holes = (result) => [...result.missing].map(([asset, names]) => ` ${asset}: нет значений для ${names.map((name) => `{{${name}}}`).join(', ')}`);
|
|
65
135
|
const unfilled = (result) => result.companions.filter(isUnfilled);
|
|
66
136
|
/** Ось без ответа: чем её спрашивают и из чего выбирают. */
|
|
@@ -105,10 +175,25 @@ const unboundLines = (result) => result.unbound.length
|
|
|
105
175
|
.map((line) => ` ${line}`),
|
|
106
176
|
]
|
|
107
177
|
: [];
|
|
178
|
+
/**
|
|
179
|
+
* Разорванные связи между ресурсами.
|
|
180
|
+
*
|
|
181
|
+
* Печатается предупреждением и кода возврата не меняет: дерево вправе закрыть требование своим
|
|
182
|
+
* средством. Но и молчать нельзя — дерево, выбравшее паттерн без хука, которым тот живёт,
|
|
183
|
+
* выглядит исправным, а расходится с пакетом на целую волну работ.
|
|
184
|
+
*/
|
|
185
|
+
const brokenLines = (result) => result.broken.length
|
|
186
|
+
? [
|
|
187
|
+
`требования ресурсов, оставшиеся без ответа: ${result.broken.length}`,
|
|
188
|
+
...result.broken.map((link) => ` ${link.id} — требует ${link.requires}: ${link.unknown ? 'нет в пакете' : NOT_CHOSEN}`),
|
|
189
|
+
' это предупреждение, а не отказ: требование закрывается своим средством дерева либо выбором ресурса',
|
|
190
|
+
]
|
|
191
|
+
: [];
|
|
108
192
|
const describe = (result) => [
|
|
109
193
|
...holes(result),
|
|
110
194
|
...gapLines(result),
|
|
111
195
|
...unboundLines(result),
|
|
196
|
+
...brokenLines(result),
|
|
112
197
|
...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
|
|
113
198
|
...unfilled(result).map((entry) => ` ${entry.path} — ${COMPANION_WORD[entry.state]}`),
|
|
114
199
|
...abandonedLines(result),
|
|
@@ -200,9 +285,12 @@ export function sync(env, check) {
|
|
|
200
285
|
const empty = unfilled(result);
|
|
201
286
|
// Гард, которого не зовёт настройка агента, — такое же расхождение, как отставший файл:
|
|
202
287
|
// он разложен, он коммитится, и по дереву его не отличить от работающего.
|
|
288
|
+
// Разорванная связь в счёт расхождений не идёт: дерево вправе закрыть требование своим
|
|
289
|
+
// средством, и отказ отбивал бы законную раскладку. Но и сходство её не отменяет —
|
|
290
|
+
// предупреждение печатается и там, где расходиться больше нечему.
|
|
203
291
|
const count = result.missing.size + result.gaps.length + pending.length + empty.length + result.unbound.length;
|
|
204
292
|
if (!count) {
|
|
205
|
-
return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}
|
|
293
|
+
return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`, ...brokenLines(result)] };
|
|
206
294
|
}
|
|
207
295
|
return { code: 1, lines: [`sync --check: расхождений ${count}`, ...describe(result)] };
|
|
208
296
|
}
|
|
@@ -298,6 +386,222 @@ export function adopt(env, names) {
|
|
|
298
386
|
],
|
|
299
387
|
};
|
|
300
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Сколько незагруженных правил называется поимённо. На коротком отрезке их бывает больше
|
|
391
|
+
* тридцати, и список во весь экран прячет всё остальное, ради чего сводку и открыли.
|
|
392
|
+
*/
|
|
393
|
+
const UNUSED_SHOWN = 12;
|
|
394
|
+
/** Столбиком: имя и счёт. Ширина по самому длинному имени — иначе счёт читается по одному. */
|
|
395
|
+
const countLines = (counted) => {
|
|
396
|
+
const width = counted.reduce((found, entry) => Math.max(found, entry.name.length), 0);
|
|
397
|
+
return counted.map((entry) => ` ${entry.name.padEnd(width)} ${entry.count}`);
|
|
398
|
+
};
|
|
399
|
+
/** Имена скилов, разложенных в это дерево: правила, паттерны и скилы без закона. */
|
|
400
|
+
const skillsOf = (config, assetsDir) => collectAssets(config, assetsDir)
|
|
401
|
+
.filter((asset) => SKILL_KINDS.includes(asset.kind))
|
|
402
|
+
.map((asset) => asset.name);
|
|
403
|
+
/**
|
|
404
|
+
* Сводка наблюдений за отрезок дней.
|
|
405
|
+
*
|
|
406
|
+
* Отдельная строка про незагруженное — то, ради чего сводка вообще нужна: чем пользуются, видно
|
|
407
|
+
* и по работе, а вот правило, которое не открыли ни разу, ничем себя не выдаёт. Сокращать текст
|
|
408
|
+
* правил так же ценно, как дополнять.
|
|
409
|
+
*/
|
|
410
|
+
export function stats(env, options) {
|
|
411
|
+
const { root, version, assetsDir } = env;
|
|
412
|
+
const config = readConfig(root);
|
|
413
|
+
if (!config) {
|
|
414
|
+
return { code: 1, lines: [NO_CONFIG] };
|
|
415
|
+
}
|
|
416
|
+
// Выключенная запись — не пустая сводка: нули на месте наблюдений читаются как «ничего не
|
|
417
|
+
// делали», тогда как на самом деле никто и не смотрел.
|
|
418
|
+
if (!config.observe) {
|
|
419
|
+
return {
|
|
420
|
+
code: 0,
|
|
421
|
+
lines: [
|
|
422
|
+
'запись наблюдений выключена ключом `observe` в конфиге',
|
|
423
|
+
`включить — убрать ключ или поставить \`"observe": true\`; пишутся они в ${OBSERVATIONS_DIR}/`,
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const days = options.days > 0 ? options.days : DEFAULT_DAYS;
|
|
428
|
+
const result = readObservations(root, options.today, days);
|
|
429
|
+
const summary = summarize(result.observations, skillsOf(config, assetsDir), days);
|
|
430
|
+
if (options.json) {
|
|
431
|
+
return { code: 0, lines: [JSON.stringify({ ...summary, swept: result.swept })] };
|
|
432
|
+
}
|
|
433
|
+
if (result.silent) {
|
|
434
|
+
return {
|
|
435
|
+
code: 0,
|
|
436
|
+
lines: [
|
|
437
|
+
'наблюдений нет: записи не велось ни разу',
|
|
438
|
+
`их пишут гарды в ${OBSERVATIONS_DIR}/ — проверь, что гарды разложены и подключены в ${SETTINGS_PATH}`,
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (!summary.total) {
|
|
443
|
+
return {
|
|
444
|
+
code: 0,
|
|
445
|
+
lines: [
|
|
446
|
+
`наблюдений за ${days} дн. нет ни одного, а записи велись раньше`,
|
|
447
|
+
...(result.swept.length ? [`снято по сроку хранения (${KEEP_DAYS} дн.): ${result.swept.length}`] : []),
|
|
448
|
+
'возьми отрезок длиннее: `--days 14`',
|
|
449
|
+
],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
code: 0,
|
|
454
|
+
lines: [
|
|
455
|
+
`наблюдения за ${days} дн., заходов ${summary.sessions}, событий ${summary.total}`,
|
|
456
|
+
...(summary.versions.length ? [`версии пакета в записях: ${summary.versions.join(', ')}`] : []),
|
|
457
|
+
'',
|
|
458
|
+
`правил загружено: ${summary.loads.reduce((found, entry) => found + entry.count, 0)}`,
|
|
459
|
+
...countLines(summary.loads),
|
|
460
|
+
...(summary.unused.length
|
|
461
|
+
? [
|
|
462
|
+
'',
|
|
463
|
+
`разложено и не загружено ни разу: ${summary.unused.length} из ${skillsOf(config, assetsDir).length}`,
|
|
464
|
+
...summary.unused.slice(0, UNUSED_SHOWN).map((name) => ` ${name}`),
|
|
465
|
+
// Список говорится не весь, и об этом говорится вслух: молчаливый обрыв
|
|
466
|
+
// читается как «вот они все», и правило, не попавшее в первую дюжину,
|
|
467
|
+
// считалось бы работающим.
|
|
468
|
+
...(summary.unused.length > UNUSED_SHOWN
|
|
469
|
+
? [` … и ещё ${summary.unused.length - UNUSED_SHOWN} — целиком в \`--json\``]
|
|
470
|
+
: []),
|
|
471
|
+
]
|
|
472
|
+
: []),
|
|
473
|
+
...(summary.denials.length
|
|
474
|
+
? [
|
|
475
|
+
'',
|
|
476
|
+
`гейт отбивал: ${summary.denials.reduce((found, entry) => found + entry.count, 0)}`,
|
|
477
|
+
...countLines(summary.denials),
|
|
478
|
+
...(summary.kinds.length
|
|
479
|
+
? [' чаще всего на:', ...countLines(summary.kinds).map((line) => ` ${line}`)]
|
|
480
|
+
: []),
|
|
481
|
+
]
|
|
482
|
+
: []),
|
|
483
|
+
...(summary.guards.length
|
|
484
|
+
? [
|
|
485
|
+
'',
|
|
486
|
+
`гарды отказывали: ${summary.guards.reduce((found, entry) => found + entry.count, 0)}`,
|
|
487
|
+
...countLines(summary.guards),
|
|
488
|
+
]
|
|
489
|
+
: []),
|
|
490
|
+
...(result.swept.length ? ['', `снято по сроку хранения (${KEEP_DAYS} дн.): ${result.swept.length}`] : []),
|
|
491
|
+
'',
|
|
492
|
+
`пакет v${version}`,
|
|
493
|
+
],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/** Тело записи: место, повод, готовый текст — и сводка под ними. */
|
|
497
|
+
const issueBody = (proposal, summary, version) => [
|
|
498
|
+
`Ресурс: \`${proposal.resource}\``,
|
|
499
|
+
`Пакет: v${version}`,
|
|
500
|
+
'',
|
|
501
|
+
proposal.body,
|
|
502
|
+
'',
|
|
503
|
+
'## Наблюдения дерева, из которого пришло предложение',
|
|
504
|
+
'',
|
|
505
|
+
'```',
|
|
506
|
+
...summary,
|
|
507
|
+
'```',
|
|
508
|
+
'',
|
|
509
|
+
'Заведено `agent-kit propose`. Дерево, из которого оно пришло, здесь не называется намеренно.',
|
|
510
|
+
].join('\n');
|
|
511
|
+
/**
|
|
512
|
+
* Отправка предложений, адресованных пакету.
|
|
513
|
+
*
|
|
514
|
+
* Наружу уезжает только адрес «пакет»: «компаньон» и «дерево» — про имена и надстройки этого
|
|
515
|
+
* дерева, и в репозитории пакета им делать нечего. Перед отправкой текст сверяется на адрес
|
|
516
|
+
* дерева: файл уезжает в чужой репозиторий целиком, и проверка здесь дешевле, чем разбор потом.
|
|
517
|
+
*/
|
|
518
|
+
export function propose(env, options) {
|
|
519
|
+
const { root, version } = env;
|
|
520
|
+
const config = readConfig(root);
|
|
521
|
+
if (!config) {
|
|
522
|
+
return { code: 1, lines: [NO_CONFIG] };
|
|
523
|
+
}
|
|
524
|
+
if (!options.repository) {
|
|
525
|
+
return { code: 1, lines: ['куда отправлять — неизвестно: в манифесте пакета нет адреса репозитория'] };
|
|
526
|
+
}
|
|
527
|
+
const all = readProposals(root);
|
|
528
|
+
const mine = all.filter((entry) => entry.address === TO_PACKAGE && !entry.sent);
|
|
529
|
+
if (!mine.length) {
|
|
530
|
+
const others = all.filter((entry) => entry.address !== TO_PACKAGE).length;
|
|
531
|
+
const sent = all.filter((entry) => Boolean(entry.sent)).length;
|
|
532
|
+
return {
|
|
533
|
+
code: 0,
|
|
534
|
+
lines: [
|
|
535
|
+
'отправлять нечего: предложений с адресом «пакет» и без пометки об отправке нет',
|
|
536
|
+
...(others ? [`с другими адресами: ${others} — они правятся здесь же, надстройкой и компаньоном`] : []),
|
|
537
|
+
...(sent ? [`уже отправлено: ${sent}`] : []),
|
|
538
|
+
...(all.length ? [] : [`предложения кладутся в ${PROPOSALS_DIR}/ — форма в шаблоне \`proposal.md\``]),
|
|
539
|
+
],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// Утечка отбивает отправку целиком, а не свой блок: файл разбирает человек, и «уехало два из
|
|
543
|
+
// трёх» он прочтёт как «всё в порядке».
|
|
544
|
+
const marks = marksOf(root, options.remote);
|
|
545
|
+
const leaked = mine.flatMap((proposal) => leaksIn(proposal.body, marks).map((leak) => ({ proposal, leak })));
|
|
546
|
+
if (leaked.length) {
|
|
547
|
+
return {
|
|
548
|
+
code: 1,
|
|
549
|
+
lines: [
|
|
550
|
+
`отправка не начата: в тексте предложений назван адрес этого дерева — ${leaked.length}`,
|
|
551
|
+
...leaked.map(({ proposal, leak }) => ` ${proposal.file}:${proposal.line + leak.line} — ${leak.why}\n ${leak.text}`),
|
|
552
|
+
'предложение уезжает в чужой репозиторий целиком: правь текст, а не обходи проверку',
|
|
553
|
+
],
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (options.dryRun) {
|
|
557
|
+
return {
|
|
558
|
+
code: 0,
|
|
559
|
+
lines: [
|
|
560
|
+
`уехало бы записей: ${mine.length} → ${options.repository}, метка ${FEEDBACK_LABEL}`,
|
|
561
|
+
...mine.map((entry) => ` ${entry.resource} (${entry.file}:${entry.line})`),
|
|
562
|
+
'сводка наблюдений едет вместе с ними',
|
|
563
|
+
],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const done = [];
|
|
567
|
+
for (const proposal of mine) {
|
|
568
|
+
let url = '';
|
|
569
|
+
try {
|
|
570
|
+
url = options.submit({
|
|
571
|
+
repository: options.repository,
|
|
572
|
+
title: `предложение: ${proposal.resource}`,
|
|
573
|
+
body: issueBody(proposal, options.summary, version),
|
|
574
|
+
label: FEEDBACK_LABEL,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
return {
|
|
579
|
+
code: 1,
|
|
580
|
+
lines: [
|
|
581
|
+
...(done.length ? [`отправлено до отказа: ${done.length}`, ...done] : []),
|
|
582
|
+
`отправка оборвалась на ${proposal.resource}: ${error.message.split('\n')[0]}`,
|
|
583
|
+
// Три причины покрывают почти все отказы, и ни одну из них не видно из
|
|
584
|
+
// сообщения помощника: он говорит про метку, доступ и сам себя одинаково глухо.
|
|
585
|
+
`проверь: помощник хостинга стоит и вошёл в учётную запись; в ${options.repository} заведена метка ${FEEDBACK_LABEL}; у учётной записи есть право заводить записи`,
|
|
586
|
+
'файл предложений остался на месте — отправленное помечено, остальное уедет повторным запуском',
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// Пометка ложится сразу, а не после всех: оборвавшаяся отправка иначе увозит половину
|
|
591
|
+
// предложений и не оставляет следа, по которому видно, какую именно.
|
|
592
|
+
const path = join(root, proposal.file);
|
|
593
|
+
writeFileSync(path, markSent(readFileSync(path, 'utf8'), proposal, url), 'utf8');
|
|
594
|
+
done.push(` ${proposal.resource} → ${url}`);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
code: 0,
|
|
598
|
+
lines: [
|
|
599
|
+
`заведено записей: ${done.length} в ${options.repository}`,
|
|
600
|
+
...done,
|
|
601
|
+
'разбирается сведением: `/agent-kit-digest` в репозитории пакета — оно и заводит задачу',
|
|
602
|
+
],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
301
605
|
export function doctor(env) {
|
|
302
606
|
const { root, version, assetsDir } = env;
|
|
303
607
|
const config = readConfig(root);
|
|
@@ -320,9 +624,16 @@ export function doctor(env) {
|
|
|
320
624
|
// Ресурс, названный и в отказе, и чужим видом, считается один раз — иначе «не выбрано»
|
|
321
625
|
// уходит в минус, а минус в отчёте читается как поломка счёта, а не как двойной счёт.
|
|
322
626
|
const skipped = config.skip.filter((id) => foreignVariant.every((entry) => entry.id !== id)).length;
|
|
627
|
+
// Невыбранное называется поимённо: число «не выбрано: 73» не отвечает ни на один вопрос,
|
|
628
|
+
// ради которого его читают, — ни какого ресурса не хватает, ни требуется ли он соседу.
|
|
629
|
+
const unchosen = catalog.filter((entry) => !isChosen(entry, config) &&
|
|
630
|
+
!config.skip.includes(entry.id) &&
|
|
631
|
+
foreignVariant.every((one) => one.id !== entry.id));
|
|
323
632
|
const lines = [
|
|
324
633
|
`пакет v${version}, везёт ресурсов ${catalog.length}, взято ${taken}`,
|
|
325
634
|
`не выбрано: ${catalog.length - taken - skipped - other}, пропущено: ${skipped}, другой вид: ${other}`,
|
|
635
|
+
...unchosen.map((entry) => ` не выбран: ${entry.id}`),
|
|
636
|
+
...profileLines(root, assetsDir, config),
|
|
326
637
|
`значений в конфиге: ${Object.keys(config.vars).length}`,
|
|
327
638
|
...chosen,
|
|
328
639
|
...[...counted].map(([outcome, count]) => `${STATE_WORD[outcome]}: ${count}`),
|