@rt-tools/agent-kit 0.5.0 → 0.5.2

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 (86) hide show
  1. package/README.md +73 -0
  2. package/assets/checks/check-doc-paths.mjs +200 -30
  3. package/assets/checks/check-specs.mjs +42 -7
  4. package/assets/checks/rt-kit-checks.config.mjs +12 -0
  5. package/assets/commands/agent-kit-digest.md +83 -0
  6. package/assets/commands/next-session.md +122 -0
  7. package/assets/commands/skill-curator.md +33 -1
  8. package/assets/defaults/gate-map.sh +23 -1
  9. package/assets/defaults/project.sh +37 -1
  10. package/assets/docs/GLOSSARY.md +77 -0
  11. package/assets/hooks/docs-guard.sh +18 -1
  12. package/assets/hooks/git-guard-delivery.sh +30 -3
  13. package/assets/hooks/git-guard-main.sh +8 -0
  14. package/assets/hooks/git-guard-push-tests.sh +8 -1
  15. package/assets/hooks/grill-gate.sh +38 -16
  16. package/assets/hooks/lint-after-edit.sh +10 -3
  17. package/assets/hooks/observe.sh +90 -0
  18. package/assets/hooks/postmortem-guard.sh +92 -0
  19. package/assets/hooks/profile-check.sh +43 -0
  20. package/assets/hooks/qa-dataid-guard.sh +9 -2
  21. package/assets/hooks/reuse-first-guard.sh +9 -2
  22. package/assets/hooks/skill-gate.sh +15 -0
  23. package/assets/hooks/skill-loaded.sh +7 -0
  24. package/assets/hooks/task-context-load.sh +16 -2
  25. package/assets/hooks/task-flow-guard.sh +9 -2
  26. package/assets/hooks/window-fill-guard.sh +157 -0
  27. package/assets/laws/code-structure.md +10 -0
  28. package/assets/laws/delivery.md +8 -1
  29. package/assets/laws/project-documentation.md +9 -0
  30. package/assets/laws/verifiability.md +9 -0
  31. package/assets/laws/work-conduct.md +47 -0
  32. package/assets/patterns/git-workflow-commit.azure.md +16 -12
  33. package/assets/patterns/git-workflow-commit.github.md +16 -12
  34. package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
  35. package/assets/patterns/spec-driven-domain.md +19 -0
  36. package/assets/patterns/task-flow-close.md +4 -4
  37. package/assets/patterns/task-flow-handoff.md +115 -0
  38. package/assets/patterns/task-flow-resume.md +14 -2
  39. package/assets/patterns/task-flow-start.md +40 -2
  40. package/assets/rules/doc-style.md +39 -1
  41. package/assets/rules/git-workflow.azure.md +39 -0
  42. package/assets/rules/git-workflow.github.md +38 -0
  43. package/assets/rules/git-workflow.gitlab.md +38 -0
  44. package/assets/rules/spec-driven.md +14 -0
  45. package/assets/rules/task-flow.md +70 -3
  46. package/assets/skills/agent-kit.md +32 -0
  47. package/assets/templates/postmortem.md +32 -0
  48. package/assets/templates/proposal.md +39 -0
  49. package/bin/agent-kit.d.ts.map +1 -1
  50. package/bin/agent-kit.js +50 -1
  51. package/bin/agent-kit.js.map +1 -1
  52. package/lib/catalog.d.ts +33 -0
  53. package/lib/catalog.d.ts.map +1 -1
  54. package/lib/catalog.js +55 -1
  55. package/lib/catalog.js.map +1 -1
  56. package/lib/commands.d.ts +41 -0
  57. package/lib/commands.d.ts.map +1 -1
  58. package/lib/commands.js +315 -3
  59. package/lib/commands.js.map +1 -1
  60. package/lib/config.d.ts +11 -1
  61. package/lib/config.d.ts.map +1 -1
  62. package/lib/config.js +6 -0
  63. package/lib/config.js.map +1 -1
  64. package/lib/hooks-map.d.ts +15 -3
  65. package/lib/hooks-map.d.ts.map +1 -1
  66. package/lib/hooks-map.js +47 -11
  67. package/lib/hooks-map.js.map +1 -1
  68. package/lib/observations.d.ts +72 -0
  69. package/lib/observations.d.ts.map +1 -0
  70. package/lib/observations.js +126 -0
  71. package/lib/observations.js.map +1 -0
  72. package/lib/proposals.d.ts +48 -0
  73. package/lib/proposals.d.ts.map +1 -0
  74. package/lib/proposals.js +111 -0
  75. package/lib/proposals.js.map +1 -0
  76. package/lib/submit.d.ts +24 -0
  77. package/lib/submit.d.ts.map +1 -0
  78. package/lib/submit.js +26 -0
  79. package/lib/submit.js.map +1 -0
  80. package/lib/sync.d.ts +9 -1
  81. package/lib/sync.d.ts.map +1 -1
  82. package/lib/sync.js +4 -6
  83. package/lib/sync.js.map +1 -1
  84. package/package.json +1 -1
  85. package/rt-tools-agent-kit-0.5.2.tgz +0 -0
  86. package/rt-tools-agent-kit-0.5.0.tgz +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Что пакет везёт — прочитанное с диска, до всякого выбора проекта.\n *\n * Каталог нужен отдельно от раскладки, потому что выбирать приходится раньше, чем раскладывать:\n * `init` спрашивает, какие законы взять, а `list` показывает и то, от чего проект отказался.\n * И там и там нужен полный набор, а `collectAssets` по устройству отдаёт уже отобранный.\n */\nimport { accessSync, constants, Dirent, readdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { KINDS, TKind } from './config.js';\nimport { IAxis, IVariant, matchesVariant, readAxes, variantOf, withoutVariant } from './variants.js';\n\nexport interface IEntryOfCatalog {\n /** Идентификатор: `laws/application/money.md`. Он же путь надстройки и ключ в `only` и `skip`. */\n readonly id: string;\n readonly kind: TKind;\n /**\n * Имя без рода, вида и расширения: `delivery`, `application/money`. Им ресурс называют в\n * строке запуска, и им же он ложится в дерево проекта.\n */\n readonly name: string;\n /** Заголовок первой строки файла; у файла без заголовка — его имя. */\n readonly title: string;\n /** Вид ресурса, если он лежит в пакете в нескольких: `git-workflow.github.md`. */\n readonly variant: IVariant | null;\n readonly text: string;\n /**\n * Запускается ли файл сам по себе. Гард зовут по пути, а не через оболочку, и файл без\n * права на запуск отвечает отказом доступа — то есть ненулевым кодом, который читается как\n * «правка отбита». Признак берётся с файла в пакете и переносится на разложенный.\n */\n readonly executable: boolean;\n}\n\nconst TITLE: RegExp = /^#\\s+(\\S.*)$/m;\n\n/** Запускается ли файл сам по себе. Спрашиваем систему, а не разбираем биты режима руками. */\nfunction isExecutable(path: string): boolean {\n try {\n accessSync(path, constants.X_OK);\n\n return true;\n } catch {\n return false;\n }\n}\n\n/** Заголовок нужен человеку, который выбирает: имена законов ему ни о чём не говорят. */\nexport function titleOf(text: string, fallback: string): string {\n const found: RegExpMatchArray | null = text.match(TITLE);\n\n return found ? found[1].trim() : fallback;\n}\n\n/**\n * Файлы рода — вместе с теми, что лежат в подкаталогах.\n *\n * Обход рекурсивный потому, что у законов два слоя: общий закон верен любому приложению этого\n * класса, а закон приложения — только такому, где есть деньги, локали или владеющая сущность.\n * Слой отражён каталогом, и путь внутри рода едет в идентификатор целиком, чтобы разложенное\n * повторяло раскладку пакета.\n */\nfunction filesOf(dir: string, prefix: string = ''): readonly string[] {\n let entries: readonly Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n\n const files: string[] = [];\n for (const entry of [...entries].sort((one: Dirent, two: Dirent): number => one.name.localeCompare(two.name))) {\n if (entry.isFile()) {\n files.push(`${prefix}${entry.name}`);\n } else if (entry.isDirectory()) {\n files.push(...filesOf(join(dir, entry.name), `${prefix}${entry.name}/`));\n }\n }\n\n return files;\n}\n\n/**\n * Всё, что лежит в ресурсах пакета. Список не выписан руками, а читается из каталога:\n * выписанный разошёлся бы с ним на первом же добавленном законе, и заметить это было бы\n * нечем — ресурс просто не приезжал бы к проекту.\n */\nexport function readCatalog(assetsDir: string): readonly IEntryOfCatalog[] {\n const axes: readonly IAxis[] = readAxes(assetsDir);\n const entries: IEntryOfCatalog[] = [];\n\n for (const kind of KINDS) {\n for (const file of filesOf(join(assetsDir, kind))) {\n const path: string = join(assetsDir, kind, file);\n const text: string = readFileSync(path, 'utf8');\n const variant: IVariant | null = variantOf(file, axes);\n const name: string = withoutVariant(file, variant).replace(/\\.[^./]+$/, '');\n const executable: boolean = isExecutable(path);\n entries.push({ id: `${kind}/${file}`, kind, name, title: titleOf(text, name), variant, text, executable });\n }\n }\n\n return entries;\n}\n\n/** Чем проект ограничил раскладку: выбором, отказом и видами, которые он назвал. */\nexport interface ISelection {\n readonly only: readonly string[];\n readonly skip: readonly string[];\n readonly variants: Readonly<Record<string, string>>;\n}\n\n/**\n * Взят ли ресурс при этом выборе.\n *\n * `only` ограничивает **только те роды, которые сам называет**. Проект, выбравший девять\n * законов из пятнадцати, шаблоны при этом не терял бы: перечисляя законы, он говорит о\n * законах, а не обо всём, что пакет везёт. Обратное правило читалось бы как «выбрал законы —\n * остался без шаблонов», и заметил бы это только тот, кто пошёл за шаблоном.\n *\n * `skip` вычитает из выбранного. Отказ от одного закона не должен требовать переписать\n * список из пятнадцати, а точечное исключение остаётся точечным.\n *\n * Вид отбирается раньше обоих: ресурс чужого хостинга не «не выбран» — его в этом дереве не\n * существует вовсе, и называть его в `skip` проекту незачем.\n */\nexport function isChosen(entry: IEntryOfCatalog, selection: ISelection): boolean {\n if (!matchesVariant(entry.variant, selection.variants)) {\n return false;\n }\n if (selection.skip.includes(entry.id)) {\n return false;\n }\n const restricted: boolean = selection.only.some((id: string): boolean => id.startsWith(`${entry.kind}/`));\n\n return !restricted || selection.only.includes(entry.id);\n}\n\n/** Ресурс, у которого виды есть, но ни один не отвечает тому, что выбрало дерево. */\nexport interface IGapOfVariant {\n readonly kind: TKind;\n /** Имя без вида — под ним ресурс лёг бы в дерево. */\n readonly name: string;\n readonly axis: string;\n /** Что выбрало дерево по этой оси. */\n readonly chosen: string;\n /** Виды, которые у ресурса есть на самом деле. */\n readonly available: readonly string[];\n /** Идентификаторы этих видов: ими же пробел и снимается через `skip`. */\n readonly ids: readonly string[];\n}\n\n/**\n * Ресурсы, оставшиеся без вида под выбор дерева.\n *\n * Молчаливый пропуск такого ресурса — худший из исходов. Правило поставки едет тремя видами, а\n * команды заведения задачи и сверки очереди — одним: дерево, выбравшее чужой хостинг, получает\n * правило, которое зовёт три команды, и ни одного скрипта под ними. Отказа при этом не бывает —\n * ресурс просто не приезжает, и узнают об этом, когда команда из правила не находится.\n *\n * Отсюда правило: **ресурс с видами, ни один из которых не совпал, — отказ, а не пропуск.**\n * Снимается он двумя способами, и оба явные: завести недостающий вид в пакете либо назвать\n * существующие в `skip` — дерево тем самым говорит, что обходится без этого ресурса.\n */\nexport function variantGaps(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IGapOfVariant[] {\n const groups: Map<string, IEntryOfCatalog[]> = new Map();\n for (const entry of catalog) {\n if (entry.variant === null) {\n continue;\n }\n const key: string = `${entry.kind}/${entry.name}`;\n groups.set(key, [...(groups.get(key) ?? []), entry]);\n }\n\n const gaps: IGapOfVariant[] = [];\n for (const entries of groups.values()) {\n const axis: string = entries[0].variant?.axis ?? '';\n if (entries.some((entry: IEntryOfCatalog): boolean => matchesVariant(entry.variant, selection.variants))) {\n continue;\n }\n // Отказ от ресурса — законный ответ, и повторять его отказом раскладки незачем: дерево\n // уже сказало, что обходится без него.\n if (entries.every((entry: IEntryOfCatalog): boolean => selection.skip.includes(entry.id))) {\n continue;\n }\n // Род, суженный через `only`, отбирает ресурсы поимённо: не названный в нём ресурс не\n // пропал — его не просили.\n const restricted: boolean = selection.only.some((id: string): boolean => id.startsWith(`${entries[0].kind}/`));\n if (restricted && !entries.some((entry: IEntryOfCatalog): boolean => selection.only.includes(entry.id))) {\n continue;\n }\n gaps.push({\n kind: entries[0].kind,\n name: entries[0].name,\n axis,\n chosen: selection.variants[axis] ?? '',\n available: entries.map((entry: IEntryOfCatalog): string => entry.variant?.value ?? ''),\n ids: entries.map((entry: IEntryOfCatalog): string => entry.id),\n });\n }\n\n return gaps;\n}\n\n/**\n * Идентификатор по тому, как ресурс назвали в строке запуска. Принимаются все формы, которыми\n * его называют вслух: `money`, `application/money`, `money.md` и `laws/application/money.md`.\n */\nexport function idOf(spoken: string, kind: TKind, catalog: readonly IEntryOfCatalog[]): string | null {\n const wanted: string = spoken.trim();\n const shortOf: (entry: IEntryOfCatalog) => string = (entry: IEntryOfCatalog): string => entry.name.split('/').pop() ?? entry.name;\n const found: IEntryOfCatalog | undefined = catalog.find(\n (entry: IEntryOfCatalog): boolean =>\n entry.kind === kind &&\n (entry.id === wanted || entry.name === wanted || shortOf(entry) === wanted || entry.id.endsWith(`/${wanted}`))\n );\n\n return found ? found.id : null;\n}\n\nexport interface IResolvedSelection {\n readonly ids: readonly string[];\n /** Имена, которым в каталоге ничего не отвечает: промах в строке запуска, а не выбор. */\n readonly unknown: readonly string[];\n}\n\n/** Разбор `--laws access,delivery`: имя без ответа — промах, и молчать о нём нельзя. */\nexport function resolveSelection(spoken: readonly string[], kind: TKind, catalog: readonly IEntryOfCatalog[]): IResolvedSelection {\n const ids: string[] = [];\n const unknown: string[] = [];\n\n for (const name of spoken) {\n const id: string | null = idOf(name, kind, catalog);\n if (id === null) {\n unknown.push(name.trim());\n } else if (!ids.includes(id)) {\n ids.push(id);\n }\n }\n\n return { ids, unknown };\n}\n"]}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Что пакет везёт — прочитанное с диска, до всякого выбора проекта.\n *\n * Каталог нужен отдельно от раскладки, потому что выбирать приходится раньше, чем раскладывать:\n * `init` спрашивает, какие законы взять, а `list` показывает и то, от чего проект отказался.\n * И там и там нужен полный набор, а `collectAssets` по устройству отдаёт уже отобранный.\n */\nimport { accessSync, constants, Dirent, readdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { KINDS, TKind } from './config.js';\nimport { IAxis, IVariant, matchesVariant, readAxes, variantOf, withoutVariant } from './variants.js';\n\nexport interface IEntryOfCatalog {\n /** Идентификатор: `laws/application/money.md`. Он же путь надстройки и ключ в `only` и `skip`. */\n readonly id: string;\n readonly kind: TKind;\n /**\n * Имя без рода, вида и расширения: `delivery`, `application/money`. Им ресурс называют в\n * строке запуска, и им же он ложится в дерево проекта.\n */\n readonly name: string;\n /** Заголовок первой строки файла; у файла без заголовка — его имя. */\n readonly title: string;\n /** Вид ресурса, если он лежит в пакете в нескольких: `git-workflow.github.md`. */\n readonly variant: IVariant | null;\n readonly text: string;\n /**\n * Запускается ли файл сам по себе. Гард зовут по пути, а не через оболочку, и файл без\n * права на запуск отвечает отказом доступа — то есть ненулевым кодом, который читается как\n * «правка отбита». Признак берётся с файла в пакете и переносится на разложенный.\n */\n readonly executable: boolean;\n /**\n * Ресурсы, без которых этот неисполним, — их идентификаторы.\n *\n * Читается из шапки самого ресурса, а не из отдельного списка при пакете: список разошёлся\n * бы с ресурсами молча, ровно тем же молчанием, ради которого требование и заведено. Паттерн\n * возвращения к работе начинается словами «хук запуска сессии отдал замысел и ход работы» —\n * без хука он неисполним, и до этой строки узнать об этом было нечем.\n */\n readonly requires: readonly string[];\n}\n\nconst TITLE: RegExp = /^#\\s+(\\S.*)$/m;\n\n/**\n * Строка требования в шапке ресурса. Двух видов, потому что ресурсы двух родов: у разметки\n * `**Требует:**`, у исполняемого файла — комментарий `# Требует:`.\n */\nconst REQUIRES: RegExp = /^(?:#\\s*Требует:|\\*\\*Требует:\\*\\*)\\s*(.+)$/m;\n\n/**\n * Что ресурс о себе объявил. Имена читаются идентификаторами — `hooks/observe.sh`: короткая\n * форма совпала бы у закона и правила с одним именем, а требование, указавшее не туда, хуже\n * ненайденного.\n */\nexport function requiresOf(text: string): readonly string[] {\n const found: RegExpMatchArray | null = text.match(REQUIRES);\n if (!found) {\n return [];\n }\n\n return found[1]\n .split(',')\n .map((name: string): string => name.trim().replace(/^`|`$/g, '').trim())\n .filter((name: string): boolean => name.length > 0);\n}\n\n/** Запускается ли файл сам по себе. Спрашиваем систему, а не разбираем биты режима руками. */\nfunction isExecutable(path: string): boolean {\n try {\n accessSync(path, constants.X_OK);\n\n return true;\n } catch {\n return false;\n }\n}\n\n/** Заголовок нужен человеку, который выбирает: имена законов ему ни о чём не говорят. */\nexport function titleOf(text: string, fallback: string): string {\n const found: RegExpMatchArray | null = text.match(TITLE);\n\n return found ? found[1].trim() : fallback;\n}\n\n/**\n * Файлы рода — вместе с теми, что лежат в подкаталогах.\n *\n * Обход рекурсивный потому, что у законов два слоя: общий закон верен любому приложению этого\n * класса, а закон приложения — только такому, где есть деньги, локали или владеющая сущность.\n * Слой отражён каталогом, и путь внутри рода едет в идентификатор целиком, чтобы разложенное\n * повторяло раскладку пакета.\n */\nfunction filesOf(dir: string, prefix: string = ''): readonly string[] {\n let entries: readonly Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n\n const files: string[] = [];\n for (const entry of [...entries].sort((one: Dirent, two: Dirent): number => one.name.localeCompare(two.name))) {\n if (entry.isFile()) {\n files.push(`${prefix}${entry.name}`);\n } else if (entry.isDirectory()) {\n files.push(...filesOf(join(dir, entry.name), `${prefix}${entry.name}/`));\n }\n }\n\n return files;\n}\n\n/**\n * Всё, что лежит в ресурсах пакета. Список не выписан руками, а читается из каталога:\n * выписанный разошёлся бы с ним на первом же добавленном законе, и заметить это было бы\n * нечем — ресурс просто не приезжал бы к проекту.\n */\nexport function readCatalog(assetsDir: string): readonly IEntryOfCatalog[] {\n const axes: readonly IAxis[] = readAxes(assetsDir);\n const entries: IEntryOfCatalog[] = [];\n\n for (const kind of KINDS) {\n for (const file of filesOf(join(assetsDir, kind))) {\n const path: string = join(assetsDir, kind, file);\n const text: string = readFileSync(path, 'utf8');\n const variant: IVariant | null = variantOf(file, axes);\n const name: string = withoutVariant(file, variant).replace(/\\.[^./]+$/, '');\n const executable: boolean = isExecutable(path);\n entries.push({\n id: `${kind}/${file}`,\n kind,\n name,\n title: titleOf(text, name),\n variant,\n text,\n executable,\n requires: requiresOf(text),\n });\n }\n }\n\n return entries;\n}\n\n/** Чем проект ограничил раскладку: выбором, отказом и видами, которые он назвал. */\nexport interface ISelection {\n readonly only: readonly string[];\n readonly skip: readonly string[];\n readonly variants: Readonly<Record<string, string>>;\n}\n\n/**\n * Взят ли ресурс при этом выборе.\n *\n * `only` ограничивает **только те роды, которые сам называет**. Проект, выбравший девять\n * законов из пятнадцати, шаблоны при этом не терял бы: перечисляя законы, он говорит о\n * законах, а не обо всём, что пакет везёт. Обратное правило читалось бы как «выбрал законы —\n * остался без шаблонов», и заметил бы это только тот, кто пошёл за шаблоном.\n *\n * `skip` вычитает из выбранного. Отказ от одного закона не должен требовать переписать\n * список из пятнадцати, а точечное исключение остаётся точечным.\n *\n * Вид отбирается раньше обоих: ресурс чужого хостинга не «не выбран» — его в этом дереве не\n * существует вовсе, и называть его в `skip` проекту незачем.\n */\nexport function isChosen(entry: IEntryOfCatalog, selection: ISelection): boolean {\n if (!matchesVariant(entry.variant, selection.variants)) {\n return false;\n }\n if (selection.skip.includes(entry.id)) {\n return false;\n }\n const restricted: boolean = selection.only.some((id: string): boolean => id.startsWith(`${entry.kind}/`));\n\n return !restricted || selection.only.includes(entry.id);\n}\n\n/** Ресурс, у которого виды есть, но ни один не отвечает тому, что выбрало дерево. */\nexport interface IGapOfVariant {\n readonly kind: TKind;\n /** Имя без вида — под ним ресурс лёг бы в дерево. */\n readonly name: string;\n readonly axis: string;\n /** Что выбрало дерево по этой оси. */\n readonly chosen: string;\n /** Виды, которые у ресурса есть на самом деле. */\n readonly available: readonly string[];\n /** Идентификаторы этих видов: ими же пробел и снимается через `skip`. */\n readonly ids: readonly string[];\n}\n\n/**\n * Ресурсы, оставшиеся без вида под выбор дерева.\n *\n * Молчаливый пропуск такого ресурса — худший из исходов. Правило поставки едет тремя видами, а\n * команды заведения задачи и сверки очереди — одним: дерево, выбравшее чужой хостинг, получает\n * правило, которое зовёт три команды, и ни одного скрипта под ними. Отказа при этом не бывает —\n * ресурс просто не приезжает, и узнают об этом, когда команда из правила не находится.\n *\n * Отсюда правило: **ресурс с видами, ни один из которых не совпал, — отказ, а не пропуск.**\n * Снимается он двумя способами, и оба явные: завести недостающий вид в пакете либо назвать\n * существующие в `skip` — дерево тем самым говорит, что обходится без этого ресурса.\n */\nexport function variantGaps(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IGapOfVariant[] {\n const groups: Map<string, IEntryOfCatalog[]> = new Map();\n for (const entry of catalog) {\n if (entry.variant === null) {\n continue;\n }\n const key: string = `${entry.kind}/${entry.name}`;\n groups.set(key, [...(groups.get(key) ?? []), entry]);\n }\n\n const gaps: IGapOfVariant[] = [];\n for (const entries of groups.values()) {\n const axis: string = entries[0].variant?.axis ?? '';\n if (entries.some((entry: IEntryOfCatalog): boolean => matchesVariant(entry.variant, selection.variants))) {\n continue;\n }\n // Отказ от ресурса — законный ответ, и повторять его отказом раскладки незачем: дерево\n // уже сказало, что обходится без него.\n if (entries.every((entry: IEntryOfCatalog): boolean => selection.skip.includes(entry.id))) {\n continue;\n }\n // Род, суженный через `only`, отбирает ресурсы поимённо: не названный в нём ресурс не\n // пропал — его не просили.\n const restricted: boolean = selection.only.some((id: string): boolean => id.startsWith(`${entries[0].kind}/`));\n if (restricted && !entries.some((entry: IEntryOfCatalog): boolean => selection.only.includes(entry.id))) {\n continue;\n }\n gaps.push({\n kind: entries[0].kind,\n name: entries[0].name,\n axis,\n chosen: selection.variants[axis] ?? '',\n available: entries.map((entry: IEntryOfCatalog): string => entry.variant?.value ?? ''),\n ids: entries.map((entry: IEntryOfCatalog): string => entry.id),\n });\n }\n\n return gaps;\n}\n\n/** Выбранный ресурс, чьё требование в дерево не поехало. */\nexport interface IBrokenLink {\n /** Кто требует — идентификатор выбранного ресурса. */\n readonly id: string;\n /** Чего не хватает — идентификатор требования. */\n readonly requires: string;\n /** Требования нет в пакете вовсе: промах в шапке ресурса, а не выбор дерева. */\n readonly unknown: boolean;\n}\n\n/**\n * Связи, разорванные выбором дерева.\n *\n * Не отказ, а предупреждение — и это главное свойство. Дерево вправе закрыть требование своим\n * средством: паттерн исполним и с чужим хуком, если тот делает то же самое. Отказ здесь отбивал\n * бы законную раскладку, а молчание оставляет дерево исправным на вид — сверка зелена на любом\n * подмножестве, сколько бы связок ни было разорвано.\n */\nexport function brokenLinks(catalog: readonly IEntryOfCatalog[], selection: ISelection): readonly IBrokenLink[] {\n const known: ReadonlySet<string> = new Set(catalog.map((entry: IEntryOfCatalog): string => entry.id));\n const chosen: ReadonlySet<string> = new Set(\n catalog.filter((entry: IEntryOfCatalog): boolean => isChosen(entry, selection)).map((entry: IEntryOfCatalog): string => entry.id)\n );\n\n const broken: IBrokenLink[] = [];\n for (const entry of catalog) {\n if (!chosen.has(entry.id)) {\n continue;\n }\n for (const required of entry.requires) {\n if (chosen.has(required)) {\n continue;\n }\n broken.push({ id: entry.id, requires: required, unknown: !known.has(required) });\n }\n }\n\n return broken;\n}\n\n/**\n * Идентификатор по тому, как ресурс назвали в строке запуска. Принимаются все формы, которыми\n * его называют вслух: `money`, `application/money`, `money.md` и `laws/application/money.md`.\n */\nexport function idOf(spoken: string, kind: TKind, catalog: readonly IEntryOfCatalog[]): string | null {\n const wanted: string = spoken.trim();\n const shortOf: (entry: IEntryOfCatalog) => string = (entry: IEntryOfCatalog): string => entry.name.split('/').pop() ?? entry.name;\n const found: IEntryOfCatalog | undefined = catalog.find(\n (entry: IEntryOfCatalog): boolean =>\n entry.kind === kind &&\n (entry.id === wanted || entry.name === wanted || shortOf(entry) === wanted || entry.id.endsWith(`/${wanted}`))\n );\n\n return found ? found.id : null;\n}\n\nexport interface IResolvedSelection {\n readonly ids: readonly string[];\n /** Имена, которым в каталоге ничего не отвечает: промах в строке запуска, а не выбор. */\n readonly unknown: readonly string[];\n}\n\n/** Разбор `--laws access,delivery`: имя без ответа — промах, и молчать о нём нельзя. */\nexport function resolveSelection(spoken: readonly string[], kind: TKind, catalog: readonly IEntryOfCatalog[]): IResolvedSelection {\n const ids: string[] = [];\n const unknown: string[] = [];\n\n for (const name of spoken) {\n const id: string | null = idOf(name, kind, catalog);\n if (id === null) {\n unknown.push(name.trim());\n } else if (!ids.includes(id)) {\n ids.push(id);\n }\n }\n\n return { ids, unknown };\n}\n"]}
package/lib/commands.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { IStaleBuild } from './freshness.js';
2
+ import { TSubmit } from './submit.js';
2
3
  export interface IOutcomeOfCommand {
3
4
  readonly code: number;
4
5
  readonly lines: readonly string[];
@@ -14,6 +15,11 @@ export interface IEnvironment {
14
15
  * тем каталогом ресурсов, который им дали, и о существовании исходников не знают.
15
16
  */
16
17
  readonly stale?: IStaleBuild | null;
18
+ /**
19
+ * `владелец/репозиторий` пакета — куда уезжают предложения. Читается из его манифеста на
20
+ * краю: зашитый в код адрес назвал бы чужое дерево в текстах пакета.
21
+ */
22
+ readonly repository?: string;
17
23
  }
18
24
  /** Куда уезжает прежнее содержимое файла, отданного пакету. */
19
25
  export declare const KEPT_SUFFIX: string;
@@ -36,6 +42,41 @@ export declare function sync(env: IEnvironment, check: boolean): IOutcomeOfComma
36
42
  * пакет кладёт своё, а прежнее остаётся на диске под именем с пометкой и ждёт разбора.
37
43
  */
38
44
  export declare function adopt(env: IEnvironment, names: readonly string[]): IOutcomeOfCommand;
45
+ /** Чем сводка отвечает: отрезок в днях, сегодняшний день и вид вывода. */
46
+ export interface IStatsOptions {
47
+ readonly days: number;
48
+ /** Сегодняшний день, `ГГГГ-ММ-ДД`. Приходит с края: команду со своими часами не проверить. */
49
+ readonly today: string;
50
+ readonly json: boolean;
51
+ }
52
+ /**
53
+ * Сводка наблюдений за отрезок дней.
54
+ *
55
+ * Отдельная строка про незагруженное — то, ради чего сводка вообще нужна: чем пользуются, видно
56
+ * и по работе, а вот правило, которое не открыли ни разу, ничем себя не выдаёт. Сокращать текст
57
+ * правил так же ценно, как дополнять.
58
+ */
59
+ export declare function stats(env: IEnvironment, options: IStatsOptions): IOutcomeOfCommand;
60
+ /** Чем отправка живёт: чем заводить запись, куда и чем себя выдаёт это дерево. */
61
+ export interface IProposeOptions {
62
+ /** Печатать, что уехало бы, и ничего не отправлять. */
63
+ readonly dryRun: boolean;
64
+ readonly submit: TSubmit;
65
+ /** `владелец/репозиторий` пакета — из его манифеста, а не из кода. */
66
+ readonly repository: string;
67
+ /** Адрес удалённого репозитория этого дерева: он тоже называет дерево. */
68
+ readonly remote: string;
69
+ /** Сводка наблюдений, которая едет вместе с предложением: без цифр это мнение. */
70
+ readonly summary: readonly string[];
71
+ }
72
+ /**
73
+ * Отправка предложений, адресованных пакету.
74
+ *
75
+ * Наружу уезжает только адрес «пакет»: «компаньон» и «дерево» — про имена и надстройки этого
76
+ * дерева, и в репозитории пакета им делать нечего. Перед отправкой текст сверяется на адрес
77
+ * дерева: файл уезжает в чужой репозиторий целиком, и проверка здесь дешевле, чем разбор потом.
78
+ */
79
+ export declare function propose(env: IEnvironment, options: IProposeOptions): IOutcomeOfCommand;
39
80
  export declare function doctor(env: IEnvironment): IOutcomeOfCommand;
40
81
  /**
41
82
  * Что пакет везёт и что из этого взято здесь.
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/commands.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAO7C,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACvC;AAoDD,+DAA+D;AAC/D,eAAO,MAAM,WAAW,EAAE,MAAyB,CAAC;AAyFpD;;;;GAIG;AACH,wBAAgB,IAAI,CAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,SAAS,MAAM,EAAO,EAC5B,QAAQ,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,EAC/C,SAAS,GAAE,MAAW,GACvB,iBAAiB,CAgCnB;AAgBD,wBAAgB,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,iBAAiB,CA2EzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,CA+CpF;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CA2C3D;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAkDzD"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/commands.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAK7C,OAAO,EAAkB,OAAO,EAAE,MAAM,aAAa,CAAC;AAKtD,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAChC;AAqDD,+DAA+D;AAC/D,eAAO,MAAM,WAAW,EAAE,MAAyB,CAAC;AAsLpD;;;;GAIG;AACH,wBAAgB,IAAI,CAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,SAAS,MAAM,EAAO,EAC5B,QAAQ,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,EAC/C,SAAS,GAAE,MAAW,GACvB,iBAAiB,CAgCnB;AAgBD,wBAAgB,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,iBAAiB,CA8EzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,CA+CpF;AAQD,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CAC1B;AAeD;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,GAAG,iBAAiB,CA2FlF;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAuBD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,GAAG,iBAAiB,CAkGtF;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAsD3D;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAkDzD"}
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';
@@ -50,6 +53,7 @@ const KIND_TITLE = {
50
53
  commands: 'КОМАНДЫ',
51
54
  workflows: 'ВОРКФЛОУ',
52
55
  templates: 'ШАБЛОНЫ',
56
+ docs: 'ДОКУМЕНТЫ',
53
57
  };
54
58
  /** Состояние компаньона словами. У заполненного слова нет: о нём говорить нечего. */
55
59
  const COMPANION_WORD = {
@@ -60,6 +64,73 @@ const COMPANION_WORD = {
60
64
  const NO_CONFIG = `нет \`${CONFIG_PATH}\` — начни с \`agent-kit init\``;
61
65
  /** Куда уезжает прежнее содержимое файла, отданного пакету. */
62
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
+ }
63
134
  const holes = (result) => [...result.missing].map(([asset, names]) => ` ${asset}: нет значений для ${names.map((name) => `{{${name}}}`).join(', ')}`);
64
135
  const unfilled = (result) => result.companions.filter(isUnfilled);
65
136
  /** Ось без ответа: чем её спрашивают и из чего выбирают. */
@@ -104,10 +175,25 @@ const unboundLines = (result) => result.unbound.length
104
175
  .map((line) => ` ${line}`),
105
176
  ]
106
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
+ : [];
107
192
  const describe = (result) => [
108
193
  ...holes(result),
109
194
  ...gapLines(result),
110
195
  ...unboundLines(result),
196
+ ...brokenLines(result),
111
197
  ...pendingOf(result).map((entry) => ` ${entry.path} — ${STATE_WORD[entry.outcome]}`),
112
198
  ...unfilled(result).map((entry) => ` ${entry.path} — ${COMPANION_WORD[entry.state]}`),
113
199
  ...abandonedLines(result),
@@ -199,9 +285,12 @@ export function sync(env, check) {
199
285
  const empty = unfilled(result);
200
286
  // Гард, которого не зовёт настройка агента, — такое же расхождение, как отставший файл:
201
287
  // он разложен, он коммитится, и по дереву его не отличить от работающего.
288
+ // Разорванная связь в счёт расхождений не идёт: дерево вправе закрыть требование своим
289
+ // средством, и отказ отбивал бы законную раскладку. Но и сходство её не отменяет —
290
+ // предупреждение печатается и там, где расходиться больше нечему.
202
291
  const count = result.missing.size + result.gaps.length + pending.length + empty.length + result.unbound.length;
203
292
  if (!count) {
204
- return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`] };
293
+ return { code: 0, lines: [`sync --check: разложенное сходится с пакетом v${version}`, ...brokenLines(result)] };
205
294
  }
206
295
  return { code: 1, lines: [`sync --check: расхождений ${count}`, ...describe(result)] };
207
296
  }
@@ -297,6 +386,222 @@ export function adopt(env, names) {
297
386
  ],
298
387
  };
299
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
+ }
300
605
  export function doctor(env) {
301
606
  const { root, version, assetsDir } = env;
302
607
  const config = readConfig(root);
@@ -319,9 +624,16 @@ export function doctor(env) {
319
624
  // Ресурс, названный и в отказе, и чужим видом, считается один раз — иначе «не выбрано»
320
625
  // уходит в минус, а минус в отчёте читается как поломка счёта, а не как двойной счёт.
321
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));
322
632
  const lines = [
323
633
  `пакет v${version}, везёт ресурсов ${catalog.length}, взято ${taken}`,
324
634
  `не выбрано: ${catalog.length - taken - skipped - other}, пропущено: ${skipped}, другой вид: ${other}`,
635
+ ...unchosen.map((entry) => ` не выбран: ${entry.id}`),
636
+ ...profileLines(root, assetsDir, config),
325
637
  `значений в конфиге: ${Object.keys(config.vars).length}`,
326
638
  ...chosen,
327
639
  ...[...counted].map(([outcome, count]) => `${STATE_WORD[outcome]}: ${count}`),