@rt-tools/agent-kit 0.5.0 → 0.5.1

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 (41) hide show
  1. package/README.md +5 -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/next-session.md +122 -0
  6. package/assets/defaults/gate-map.sh +21 -1
  7. package/assets/defaults/project.sh +25 -0
  8. package/assets/docs/GLOSSARY.md +74 -0
  9. package/assets/hooks/window-fill-guard.sh +150 -0
  10. package/assets/laws/code-structure.md +10 -0
  11. package/assets/laws/delivery.md +8 -1
  12. package/assets/laws/project-documentation.md +4 -0
  13. package/assets/laws/work-conduct.md +8 -0
  14. package/assets/patterns/spec-driven-domain.md +19 -0
  15. package/assets/patterns/task-flow-close.md +4 -4
  16. package/assets/patterns/task-flow-handoff.md +115 -0
  17. package/assets/patterns/task-flow-resume.md +2 -2
  18. package/assets/patterns/task-flow-start.md +4 -0
  19. package/assets/rules/doc-style.md +39 -1
  20. package/assets/rules/git-workflow.azure.md +24 -0
  21. package/assets/rules/git-workflow.github.md +23 -0
  22. package/assets/rules/git-workflow.gitlab.md +23 -0
  23. package/assets/rules/spec-driven.md +8 -0
  24. package/assets/rules/task-flow.md +46 -0
  25. package/lib/commands.d.ts.map +1 -1
  26. package/lib/commands.js +1 -0
  27. package/lib/commands.js.map +1 -1
  28. package/lib/config.d.ts +3 -1
  29. package/lib/config.d.ts.map +1 -1
  30. package/lib/config.js +2 -0
  31. package/lib/config.js.map +1 -1
  32. package/lib/hooks-map.d.ts +15 -3
  33. package/lib/hooks-map.d.ts.map +1 -1
  34. package/lib/hooks-map.js +47 -11
  35. package/lib/hooks-map.js.map +1 -1
  36. package/lib/sync.d.ts.map +1 -1
  37. package/lib/sync.js +2 -5
  38. package/lib/sync.js.map +1 -1
  39. package/package.json +1 -1
  40. package/rt-tools-agent-kit-0.5.1.tgz +0 -0
  41. package/rt-tools-agent-kit-0.5.0.tgz +0 -0
package/lib/hooks-map.js CHANGED
@@ -18,11 +18,20 @@ import { existsSync, readFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  /** Настройка агента, в которой живёт карта. Путь от корня дерева. */
20
20
  export const SETTINGS_PATH = '.claude/settings.json';
21
- const DECLARATION = /^#\s*rt-hook:\s*(\S+)(?:[ \t]+(\S.*))?$/m;
22
- /** Что гард говорит о себе. Молчит — он не подключается к агенту вовсе. */
23
- export function bindingOf(text, path) {
24
- const found = text.match(DECLARATION);
25
- return found ? { event: found[1], matcher: (found[2] ?? '').trim(), path } : null;
21
+ const DECLARATION = /^#\s*rt-hook:\s*(\S+)(?:[ \t]+(\S.*))?$/gm;
22
+ /**
23
+ * Что гард говорит о себе. Молчит — он не подключается к агенту вовсе.
24
+ *
25
+ * Объявлений бывает несколько: гард, который сперва напоминает, а потом отбивает, стоит на двух
26
+ * событиях сразу. Разводить его по двум файлам значило бы держать два разбора одной записи и
27
+ * два места, где правится один порог.
28
+ */
29
+ export function bindingsOf(text, path) {
30
+ return [...text.matchAll(DECLARATION)].map((found) => ({
31
+ event: found[1],
32
+ matcher: (found[2] ?? '').trim(),
33
+ path,
34
+ }));
26
35
  }
27
36
  /**
28
37
  * Раздел `hooks` для настройки агента: событие → образец → команда.
@@ -64,14 +73,41 @@ export function boundInSettings(root) {
64
73
  catch {
65
74
  return [];
66
75
  }
67
- // Настройка читается текстом, а не разбором: у неё нет объявленной формы, и дерево вправе
68
- // держать её так, как ему удобно,вплоть до комментариев, которых JSON не разбирает. Ищется
69
- // одно: назван ли путь гарда хоть где-нибудь в ней.
70
- return [...text.matchAll(/[\w./$-]*\.claude\/hooks\/[\w.-]+\.sh/g)].map((found) => found[0].replace(/^.*?(\.claude\/)/, '$1'));
76
+ // Сперва разбором: гард, стоящий на двух событиях, подключается к каждому отдельно, и по
77
+ // одному имени файла этого не увидетьподключённый к первому событию выглядел бы
78
+ // подключённым и ко второму.
79
+ const byEvent = [];
80
+ try {
81
+ const settings = JSON.parse(text);
82
+ const hooks = settings?.['hooks'];
83
+ for (const [event, records] of Object.entries((hooks ?? {}))) {
84
+ for (const record of Array.isArray(records) ? records : []) {
85
+ const commands = record?.['hooks'];
86
+ for (const command of Array.isArray(commands) ? commands : []) {
87
+ const line = command?.['command'];
88
+ if (typeof line === 'string') {
89
+ byEvent.push(`${event} ${line.replace(/^.*?(\.claude\/)/, '$1')}`);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ return byEvent;
95
+ }
96
+ catch {
97
+ // Настройку дерево вправе держать так, как ему удобно, — вплоть до комментариев, которых
98
+ // JSON не разбирает. Тогда ищется одно: назван ли путь гарда хоть где-нибудь в ней.
99
+ return [...text.matchAll(/[\w./$-]*\.claude\/hooks\/[\w.-]+\.sh/g)].map((found) => found[0].replace(/^.*?(\.claude\/)/, '$1'));
100
+ }
71
101
  }
72
- /** Разложенные гарды, которых в настройке агента нет: они лежат, но их никто не позовёт. */
102
+ /**
103
+ * Разложенные гарды, которых в настройке агента нет: они лежат, но их никто не позовёт.
104
+ *
105
+ * Разбор настройки называет пару «событие и путь», а падение назад — один путь. Поэтому
106
+ * подключённым считается и то, и другое: иначе дерево с настройкой, которую не разобрать,
107
+ * получало бы список из всех гардов разом.
108
+ */
73
109
  export function unboundHooks(bindings, root) {
74
110
  const bound = new Set(boundInSettings(root));
75
- return bindings.filter((binding) => !bound.has(binding.path));
111
+ return bindings.filter((binding) => !bound.has(`${binding.event} ${binding.path}`) && !bound.has(binding.path));
76
112
  }
77
113
  //# sourceMappingURL=hooks-map.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-map.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/hooks-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,qEAAqE;AACrE,MAAM,CAAC,MAAM,aAAa,GAAW,uBAAuB,CAAC;AAE7D,MAAM,WAAW,GAAW,0CAA0C,CAAC;AAcvE,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY;IAChD,MAAM,KAAK,GAA4B,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAE/D,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,QAAiC;IAC1D,MAAM,MAAM,GAAuC,IAAI,GAAG,EAAE,CAAC;IAE7D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,SAAS,GAA0B,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;QAChF,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1F,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAClD,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAA8B,EAAW,EAAE;YAClG,MAAM,KAAK,GAAc,KAAK,CAAC,GAAG,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC;gBAC3D,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,uBAAuB,IAAI,EAAE;aACzC,CAAC,CAAC,CAAC;YAEJ,mFAAmF;YACnF,6EAA6E;YAC7E,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,eAAe,CAAC,IAAY;IACxC,MAAM,IAAI,GAAW,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,CAAC;IACd,CAAC;IACD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACD,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;IAED,0FAA0F;IAC1F,8FAA8F;IAC9F,oDAAoD;IACpD,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,wCAAwC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAuB,EAAU,EAAE,CACxG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAC7C,CAAC;AACN,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY,CAAC,QAAiC,EAAE,IAAY;IACxE,MAAM,KAAK,GAAwB,IAAI,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IAElE,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAqB,EAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACzF,CAAC","sourcesContent":["/**\n * Карта хуков: чем разложенные гарды подключаются к агенту.\n *\n * Раскладка кладёт файлы, а зовёт их запись в настройке агента — чужом JSON, которым пакет не\n * владеет. Пока о ней молчали, свежее дерево получало два десятка гардов, ни один из которых не\n * срабатывал, и узнавало об этом, когда что-нибудь проходило мимо.\n *\n * Пакет не пишет в чужую настройку сам: файл держит проект, в нём лежит и то, о чём пакет не\n * знает вовсе, а слияние чужого JSON молча теряет несовпавшее. Вместо этого он собирает готовый\n * кусок и проверяет, что он вставлен. Это дешевле и честнее: правку в свою настройку делает тот,\n * кто за неё отвечает.\n *\n * Событие и образец вызова гард несёт сам — строкой `# rt-hook: <событие> <образец>` во второй\n * строке файла. Карта, выписанная отдельным списком, разошлась бы с набором гардов на первом же\n * добавленном, и заметить это было бы нечем: гард просто не звался бы.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Настройка агента, в которой живёт карта. Путь от корня дерева. */\nexport const SETTINGS_PATH: string = '.claude/settings.json';\n\nconst DECLARATION: RegExp = /^#\\s*rt-hook:\\s*(\\S+)(?:[ \\t]+(\\S.*))?$/m;\n\nexport interface IHookBinding {\n /** Событие агента: `PreToolUse`, `PostToolUse`, `SessionStart`, `Stop`. */\n readonly event: string;\n /**\n * Образец, по которому событие достаётся этому гарду. Пустой у события, которое не про\n * инструмент: завершение хода приходит целиком, и выбирать в нём нечего.\n */\n readonly matcher: string;\n /** Путь разложенного гарда от корня дерева. */\n readonly path: string;\n}\n\n/** Что гард говорит о себе. Молчит — он не подключается к агенту вовсе. */\nexport function bindingOf(text: string, path: string): IHookBinding | null {\n const found: RegExpMatchArray | null = text.match(DECLARATION);\n\n return found ? { event: found[1], matcher: (found[2] ?? '').trim(), path } : null;\n}\n\n/**\n * Раздел `hooks` для настройки агента: событие → образец → команда.\n *\n * Гарды с одинаковым образцом внутри события собираются в одну запись — так же, как это пишут\n * руками, и так же, как читает агент.\n */\nexport function hooksSection(bindings: readonly IHookBinding[]): Record<string, unknown> {\n const events: Map<string, Map<string, string[]>> = new Map();\n\n for (const binding of bindings) {\n const byMatcher: Map<string, string[]> = events.get(binding.event) ?? new Map();\n byMatcher.set(binding.matcher, [...(byMatcher.get(binding.matcher) ?? []), binding.path]);\n events.set(binding.event, byMatcher);\n }\n\n const section: Record<string, unknown> = {};\n for (const [event, byMatcher] of [...events].sort()) {\n section[event] = [...byMatcher].sort().map(([matcher, paths]: [string, readonly string[]]): unknown => {\n const hooks: unknown[] = paths.map((path: string): unknown => ({\n type: 'command',\n command: `$CLAUDE_PROJECT_DIR/${path}`,\n }));\n\n // Запись без образца — не запись с пустым образцом: агент читает пустую строку как\n // образец, которому не соответствует ни один вызов, и гард молча не зовётся.\n return matcher ? { matcher, hooks } : { hooks };\n });\n }\n\n return section;\n}\n\n/** Пути гардов, названные в настройке агента этого дерева. Нет файла — ни одного. */\nexport function boundInSettings(root: string): readonly string[] {\n const path: string = join(root, SETTINGS_PATH);\n if (!existsSync(path)) {\n return [];\n }\n let text: string;\n try {\n text = readFileSync(path, 'utf8');\n } catch {\n return [];\n }\n\n // Настройка читается текстом, а не разбором: у неё нет объявленной формы, и дерево вправе\n // держать её так, как ему удобно, — вплоть до комментариев, которых JSON не разбирает. Ищется\n // одно: назван ли путь гарда хоть где-нибудь в ней.\n return [...text.matchAll(/[\\w./$-]*\\.claude\\/hooks\\/[\\w.-]+\\.sh/g)].map((found: RegExpMatchArray): string =>\n found[0].replace(/^.*?(\\.claude\\/)/, '$1')\n );\n}\n\n/** Разложенные гарды, которых в настройке агента нет: они лежат, но их никто не позовёт. */\nexport function unboundHooks(bindings: readonly IHookBinding[], root: string): readonly IHookBinding[] {\n const bound: ReadonlySet<string> = new Set(boundInSettings(root));\n\n return bindings.filter((binding: IHookBinding): boolean => !bound.has(binding.path));\n}\n"]}
1
+ {"version":3,"file":"hooks-map.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/hooks-map.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Карта хуков: чем разложенные гарды подключаются к агенту.\n *\n * Раскладка кладёт файлы, а зовёт их запись в настройке агента — чужом JSON, которым пакет не\n * владеет. Пока о ней молчали, свежее дерево получало два десятка гардов, ни один из которых не\n * срабатывал, и узнавало об этом, когда что-нибудь проходило мимо.\n *\n * Пакет не пишет в чужую настройку сам: файл держит проект, в нём лежит и то, о чём пакет не\n * знает вовсе, а слияние чужого JSON молча теряет несовпавшее. Вместо этого он собирает готовый\n * кусок и проверяет, что он вставлен. Это дешевле и честнее: правку в свою настройку делает тот,\n * кто за неё отвечает.\n *\n * Событие и образец вызова гард несёт сам — строкой `# rt-hook: <событие> <образец>` во второй\n * строке файла. Карта, выписанная отдельным списком, разошлась бы с набором гардов на первом же\n * добавленном, и заметить это было бы нечем: гард просто не звался бы.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Настройка агента, в которой живёт карта. Путь от корня дерева. */\nexport const SETTINGS_PATH: string = '.claude/settings.json';\n\nconst DECLARATION: RegExp = /^#\\s*rt-hook:\\s*(\\S+)(?:[ \\t]+(\\S.*))?$/gm;\n\nexport interface IHookBinding {\n /** Событие агента: `PreToolUse`, `PostToolUse`, `SessionStart`, `Stop`. */\n readonly event: string;\n /**\n * Образец, по которому событие достаётся этому гарду. Пустой у события, которое не про\n * инструмент: завершение хода приходит целиком, и выбирать в нём нечего.\n */\n readonly matcher: string;\n /** Путь разложенного гарда от корня дерева. */\n readonly path: string;\n}\n\n/**\n * Что гард говорит о себе. Молчит — он не подключается к агенту вовсе.\n *\n * Объявлений бывает несколько: гард, который сперва напоминает, а потом отбивает, стоит на двух\n * событиях сразу. Разводить его по двум файлам значило бы держать два разбора одной записи и\n * два места, где правится один порог.\n */\nexport function bindingsOf(text: string, path: string): readonly IHookBinding[] {\n return [...text.matchAll(DECLARATION)].map((found: RegExpMatchArray): IHookBinding => ({\n event: found[1],\n matcher: (found[2] ?? '').trim(),\n path,\n }));\n}\n\n/**\n * Раздел `hooks` для настройки агента: событие → образец → команда.\n *\n * Гарды с одинаковым образцом внутри события собираются в одну запись — так же, как это пишут\n * руками, и так же, как читает агент.\n */\nexport function hooksSection(bindings: readonly IHookBinding[]): Record<string, unknown> {\n const events: Map<string, Map<string, string[]>> = new Map();\n\n for (const binding of bindings) {\n const byMatcher: Map<string, string[]> = events.get(binding.event) ?? new Map();\n byMatcher.set(binding.matcher, [...(byMatcher.get(binding.matcher) ?? []), binding.path]);\n events.set(binding.event, byMatcher);\n }\n\n const section: Record<string, unknown> = {};\n for (const [event, byMatcher] of [...events].sort()) {\n section[event] = [...byMatcher].sort().map(([matcher, paths]: [string, readonly string[]]): unknown => {\n const hooks: unknown[] = paths.map((path: string): unknown => ({\n type: 'command',\n command: `$CLAUDE_PROJECT_DIR/${path}`,\n }));\n\n // Запись без образца — не запись с пустым образцом: агент читает пустую строку как\n // образец, которому не соответствует ни один вызов, и гард молча не зовётся.\n return matcher ? { matcher, hooks } : { hooks };\n });\n }\n\n return section;\n}\n\n/** Пути гардов, названные в настройке агента этого дерева. Нет файла — ни одного. */\nexport function boundInSettings(root: string): readonly string[] {\n const path: string = join(root, SETTINGS_PATH);\n if (!existsSync(path)) {\n return [];\n }\n let text: string;\n try {\n text = readFileSync(path, 'utf8');\n } catch {\n return [];\n }\n\n // Сперва разбором: гард, стоящий на двух событиях, подключается к каждому отдельно, и по\n // одному имени файла этого не увидеть подключённый к первому событию выглядел бы\n // подключённым и ко второму.\n const byEvent: string[] = [];\n try {\n const settings: unknown = JSON.parse(text);\n const hooks: unknown = (settings as Record<string, unknown> | null)?.['hooks'];\n for (const [event, records] of Object.entries((hooks ?? {}) as Record<string, unknown>)) {\n for (const record of Array.isArray(records) ? records : []) {\n const commands: unknown = (record as Record<string, unknown> | null)?.['hooks'];\n for (const command of Array.isArray(commands) ? commands : []) {\n const line: unknown = (command as Record<string, unknown> | null)?.['command'];\n if (typeof line === 'string') {\n byEvent.push(`${event} ${line.replace(/^.*?(\\.claude\\/)/, '$1')}`);\n }\n }\n }\n }\n\n return byEvent;\n } catch {\n // Настройку дерево вправе держать так, как ему удобно, — вплоть до комментариев, которых\n // JSON не разбирает. Тогда ищется одно: назван ли путь гарда хоть где-нибудь в ней.\n return [...text.matchAll(/[\\w./$-]*\\.claude\\/hooks\\/[\\w.-]+\\.sh/g)].map((found: RegExpMatchArray): string =>\n found[0].replace(/^.*?(\\.claude\\/)/, '$1')\n );\n }\n}\n\n/**\n * Разложенные гарды, которых в настройке агента нет: они лежат, но их никто не позовёт.\n *\n * Разбор настройки называет пару «событие и путь», а падение назад — один путь. Поэтому\n * подключённым считается и то, и другое: иначе дерево с настройкой, которую не разобрать,\n * получало бы список из всех гардов разом.\n */\nexport function unboundHooks(bindings: readonly IHookBinding[], root: string): readonly IHookBinding[] {\n const bound: ReadonlySet<string> = new Set(boundInSettings(root));\n\n return bindings.filter((binding: IHookBinding): boolean => !bound.has(`${binding.event} ${binding.path}`) && !bound.has(binding.path));\n}\n"]}
package/lib/sync.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"AAWA,OAAO,EAAmB,aAAa,EAA4B,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,OAAO,EAAiB,MAAM,aAAa,CAAC;AACrD,OAAO,EAAa,YAAY,EAAgB,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAkC,MAAM,WAAW,CAAC;AAMrE,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IACzD,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AA2DD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CA6BvG;AAED,mGAAmG;AACnG,wBAAgB,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CA0CtG;AAED,eAAO,MAAM,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,QAAQ,EACe,CAAC"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"AAWA,OAAO,EAAmB,aAAa,EAA4B,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,OAAO,EAAiB,MAAM,aAAa,CAAC;AACrD,OAAO,EAA4B,YAAY,EAAgB,MAAM,gBAAgB,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAkC,MAAM,WAAW,CAAC;AAMrE,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IACzD,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAwDD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CA6BvG;AAED,mGAAmG;AACnG,wBAAgB,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CA0CtG;AAED,eAAO,MAAM,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,QAAQ,EACe,CAAC"}
package/lib/sync.js CHANGED
@@ -11,7 +11,7 @@ import { collectAssets, targetOf } from './assets.js';
11
11
  import { readCatalog, variantGaps } from './catalog.js';
12
12
  import { pathOf, planCompanion } from './companion.js';
13
13
  import { OVERRIDES_DIR } from './config.js';
14
- import { bindingOf, unboundHooks } from './hooks-map.js';
14
+ import { bindingsOf as declaredIn, unboundHooks } from './hooks-map.js';
15
15
  import { isPending, isRefusal, planFile } from './plan.js';
16
16
  import { mergeDocuments, parseDocument, renderDocument } from './sections.js';
17
17
  import { readStamped } from './stamp.js';
@@ -58,10 +58,7 @@ function bindingsOf(config, assetsDir) {
58
58
  if (asset.kind !== 'hooks') {
59
59
  continue;
60
60
  }
61
- const binding = bindingOf(asset.text, asset.target);
62
- if (binding) {
63
- bindings.push(binding);
64
- }
61
+ bindings.push(...declaredIn(asset.text, asset.target));
65
62
  }
66
63
  return bindings;
67
64
  }
package/lib/sync.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAU,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAkC,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EAAc,MAAM,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAW,aAAa,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,SAAS,EAAgB,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAY,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAiB,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAmC/C,gFAAgF;AAChF,MAAM,kBAAkB,GAAW,6BAA6B,CAAC;AAEjE,MAAM,IAAI,GAAoC,CAAC,IAAY,EAAiB,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAEtI;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAa,EAAE,MAAe,EAAE,IAAY;IAC7D,MAAM,QAAQ,GAAkB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAW,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;IAElI,OAAO,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,MAAe,EAAE,IAAY,EAAE,SAAiB;IACjE,MAAM,KAAK,GAAwB,IAAI,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAEtH,OAAO,WAAW,CAAC,SAAS,CAAC;SACxB,MAAM,CAAC,CAAC,KAAsB,EAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;SACnH,GAAG,CAAC,CAAC,KAAsB,EAAU,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;SACvE,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE;QAC9B,MAAM,QAAQ,GAAkB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAEvD,OAAO,QAAQ,KAAK,IAAI,IAAI,WAAW,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;IAC/D,CAAC,CAAC,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,MAAe,EAAE,SAAiB;IAClD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACzB,SAAS;QACb,CAAC;QACD,MAAM,OAAO,GAAwB,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACzE,IAAI,OAAO,EAAE,CAAC;YACV,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAe,EAAE,IAAY,EAAE,OAAe,EAAE,SAAiB;IACtF,MAAM,OAAO,GAAe,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAmC,IAAI,GAAG,EAAE,CAAC;IAC1D,MAAM,UAAU,GAAiB,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAkB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAE1E,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAkB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;QACjE,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YACxC,SAAS;QACb,CAAC;QACD,OAAO,CAAC,IAAI,CACR,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAChI,CAAC;QACF,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC9C,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QACrF,CAAC;IACL,CAAC;IAED,OAAO;QACH,OAAO;QACP,OAAO;QACP,UAAU;QACV,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC;QAC/C,IAAI,EAAE,WAAW,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QACjD,OAAO,EAAE,YAAY,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC;QAC1D,OAAO,EAAE,EAAE;KACd,CAAC;AACN,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,OAAO,CAAC,MAAe,EAAE,IAAY,EAAE,OAAe,EAAE,SAAiB;IACrF,MAAM,MAAM,GAAgB,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACvE,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAe,EAAW,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QAC3H,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,2FAA2F;IAC3F,0FAA0F;IAC1F,yFAAyF;IACzF,MAAM,UAAU,GAAwB,IAAI,GAAG,CAC3C,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC;SAC3B,MAAM,CAAC,CAAC,KAAa,EAAW,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC;SACpD,GAAG,CAAC,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAChD,CAAC;IAEF,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACzB,SAAS;QACb,CAAC;QACD,MAAM,IAAI,GAAW,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC3C,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,0FAA0F;IAC1F,mFAAmF;IACnF,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,SAAS,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC7B,SAAS;QACb,CAAC;QACD,MAAM,IAAI,GAAW,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAChD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAiD,CAAC,MAAmB,EAAuB,EAAE,CAChH,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAe,EAAW,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC","sourcesContent":["/**\n * Раскладка ресурсов пакета в дерево проекта.\n *\n * Порядок один и тот же и для записи, и для проверки: собрать тело, решить судьбу файла, и\n * только потом писать. `sync --check` отличается от `sync` ровно последним шагом — иначе гейт\n * пуша проверял бы не то, что кладёт раскладка.\n */\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { collectAssets, IAsset, targetOf } from './assets.js';\nimport { IEntryOfCatalog, IGapOfVariant, readCatalog, variantGaps } from './catalog.js';\nimport { ICompanion, pathOf, planCompanion } from './companion.js';\nimport { IConfig, OVERRIDES_DIR } from './config.js';\nimport { bindingOf, IHookBinding, unboundHooks } from './hooks-map.js';\nimport { IPlanned, isPending, isRefusal, planFile } from './plan.js';\nimport { mergeDocuments, parseDocument, renderDocument } from './sections.js';\nimport { readStamped } from './stamp.js';\nimport { IRenderResult, renderVars } from './vars.js';\nimport { matchesVariant } from './variants.js';\n\nexport interface ISyncResult {\n readonly planned: readonly IPlanned[];\n /** Дырки без значения, по ресурсам. Непустой список — отказ: подставлять нечего. */\n readonly missing: ReadonlyMap<string, readonly string[]>;\n /** Компаньоны разложенных правил: имена этого дерева, которые пишет проект. */\n readonly companions: readonly ICompanion[];\n /**\n * Файлы, лежащие в дереве от ресурсов, от которых проект отказался.\n *\n * Отказ значит «больше не клади», а не «убери»: разложенное коммитится, рядом с правилом\n * лежит написанный проектом компаньон, и стирать это молча пакет не вправе. Но и молчать\n * нельзя — брошенный файл читается как действующее правило, и агент по нему работает.\n */\n readonly abandoned: readonly string[];\n /**\n * Ресурсы, у которых виды есть, а вида под выбор дерева нет.\n *\n * Пропустить такой ресурс молча значит разложить правило и не разложить инструмент, который\n * оно зовёт: дерево получает указание без исполнителя и узнаёт об этом, когда команда из\n * правила не находится. Поэтому непустой список — отказ раскладки наравне с дыркой без\n * значения и с файлом, который правили руками.\n */\n readonly gaps: readonly IGapOfVariant[];\n /**\n * Разложенные гарды, которых нет в настройке агента: файл лежит, а позвать его некому.\n *\n * Раскладку это не отбивает — настройка принадлежит дереву, и пакет в неё не пишет, — но и\n * молчать нельзя: гард, который не зовут, неотличим от гарда, который всё пропускает.\n */\n readonly unbound: readonly IHookBinding[];\n readonly written: readonly string[];\n}\n\n/** Шаблон черновика компаньона — тот же, что пакет кладёт проекту в шаблоны. */\nconst COMPANION_TEMPLATE: string = 'templates/implementation.md';\n\nconst read: (path: string) => string | null = (path: string): string | null => (existsSync(path) ? readFileSync(path, 'utf8') : null);\n\n/**\n * Тело ресурса: текст пакета, поверх него надстройка проекта, и уже потом подстановка значений.\n *\n * Порядок именно такой. Надстройка тоже пишется с дырками — иначе проект, дописавший раздел про\n * свою главную ветку, зашил бы её имя в двух местах: в конфиге и в тексте.\n */\nfunction renderAsset(asset: IAsset, config: IConfig, root: string): IRenderResult {\n const override: string | null = read(join(root, OVERRIDES_DIR, asset.id));\n const merged: string = override ? renderDocument(mergeDocuments(parseDocument(asset.text), parseDocument(override))) : asset.text;\n\n return renderVars(merged, config.vars);\n}\n\n/**\n * Что лежит в дереве от ресурсов, которые проект больше не берёт.\n *\n * Ищется по тем же правилам раскладки, что и всё остальное: путь ресурса вычисляется так, будто\n * его кладут, и проверяется, лежит ли там файл с шапкой пакета. Чужой файл на том же пути\n * брошенным не считается — его пакет не клал.\n */\nfunction abandonedOf(config: IConfig, root: string, assetsDir: string): readonly string[] {\n const taken: ReadonlySet<string> = new Set(collectAssets(config, assetsDir).map((asset: IAsset): string => asset.id));\n\n return readCatalog(assetsDir)\n .filter((entry: IEntryOfCatalog): boolean => !taken.has(entry.id) && matchesVariant(entry.variant, config.variants))\n .map((entry: IEntryOfCatalog): string => targetOf(entry, config.layout))\n .filter((path: string): boolean => {\n const existing: string | null = read(join(root, path));\n\n return existing !== null && readStamped(existing) !== null;\n });\n}\n\n/**\n * Что о себе говорят гарды, которые дерево берёт. Читается у ресурсов, а не у разложенных\n * файлов: карта нужна и до первой раскладки — чтобы было что вставить в настройку.\n */\nfunction bindingsOf(config: IConfig, assetsDir: string): readonly IHookBinding[] {\n const bindings: IHookBinding[] = [];\n for (const asset of collectAssets(config, assetsDir)) {\n if (asset.kind !== 'hooks') {\n continue;\n }\n const binding: IHookBinding | null = bindingOf(asset.text, asset.target);\n if (binding) {\n bindings.push(binding);\n }\n }\n\n return bindings;\n}\n\nexport function planSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const planned: IPlanned[] = [];\n const missing: Map<string, readonly string[]> = new Map();\n const companions: ICompanion[] = [];\n const template: string | null = read(join(assetsDir, COMPANION_TEMPLATE));\n\n for (const asset of collectAssets(config, assetsDir)) {\n const rendered: IRenderResult = renderAsset(asset, config, root);\n if (rendered.missing.length) {\n missing.set(asset.id, rendered.missing);\n continue;\n }\n planned.push(\n planFile({ path: asset.target, asset: asset.id, version, rendered: rendered.text, existing: read(join(root, asset.target)) })\n );\n if (asset.kind === 'rules' && template !== null) {\n companions.push(planCompanion(asset, read(join(root, pathOf(asset))), template));\n }\n }\n\n return {\n planned,\n missing,\n companions,\n abandoned: abandonedOf(config, root, assetsDir),\n gaps: variantGaps(readCatalog(assetsDir), config),\n unbound: unboundHooks(bindingsOf(config, assetsDir), root),\n written: [],\n };\n}\n\n/** Раскладка. Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого. */\nexport function runSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n if (result.missing.size || result.gaps.length || result.planned.some((entry: IPlanned): boolean => isRefusal(entry.outcome))) {\n return result;\n }\n\n // Право на запуск переносится с файла в пакете. Без него гард отвечает отказом доступа, то\n // есть ненулевым кодом, а ненулевой код у гарда значит «правка отбита»: разложенный набор\n // отбивал бы подряд всё, включая сборку и тесты, и причина при этом нигде не называлась.\n const executable: ReadonlySet<string> = new Set(\n collectAssets(config, assetsDir)\n .filter((asset: IAsset): boolean => asset.executable)\n .map((asset: IAsset): string => asset.id)\n );\n\n const written: string[] = [];\n for (const entry of result.planned) {\n if (entry.content === null) {\n continue;\n }\n const path: string = join(root, entry.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, entry.content, 'utf8');\n if (executable.has(entry.asset)) {\n chmodSync(path, 0o755);\n }\n written.push(entry.path);\n }\n\n // Черновик компаньона кладётся только там, где файла нет вовсе. Он без шапки и без суммы:\n // сверять в нём нечего — с первой правки проекта это его текст, а не текст пакета.\n for (const companion of result.companions) {\n if (companion.content === null) {\n continue;\n }\n const path: string = join(root, companion.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, companion.content, 'utf8');\n written.push(companion.path);\n }\n\n return { ...result, written };\n}\n\nexport const pendingOf: (result: ISyncResult) => readonly IPlanned[] = (result: ISyncResult): readonly IPlanned[] =>\n result.planned.filter((entry: IPlanned): boolean => isPending(entry.outcome));\n"]}
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Раскладка ресурсов пакета в дерево проекта.\n *\n * Порядок один и тот же и для записи, и для проверки: собрать тело, решить судьбу файла, и\n * только потом писать. `sync --check` отличается от `sync` ровно последним шагом — иначе гейт\n * пуша проверял бы не то, что кладёт раскладка.\n */\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { collectAssets, IAsset, targetOf } from './assets.js';\nimport { IEntryOfCatalog, IGapOfVariant, readCatalog, variantGaps } from './catalog.js';\nimport { ICompanion, pathOf, planCompanion } from './companion.js';\nimport { IConfig, OVERRIDES_DIR } from './config.js';\nimport { bindingsOf as declaredIn, IHookBinding, unboundHooks } from './hooks-map.js';\nimport { IPlanned, isPending, isRefusal, planFile } from './plan.js';\nimport { mergeDocuments, parseDocument, renderDocument } from './sections.js';\nimport { readStamped } from './stamp.js';\nimport { IRenderResult, renderVars } from './vars.js';\nimport { matchesVariant } from './variants.js';\n\nexport interface ISyncResult {\n readonly planned: readonly IPlanned[];\n /** Дырки без значения, по ресурсам. Непустой список — отказ: подставлять нечего. */\n readonly missing: ReadonlyMap<string, readonly string[]>;\n /** Компаньоны разложенных правил: имена этого дерева, которые пишет проект. */\n readonly companions: readonly ICompanion[];\n /**\n * Файлы, лежащие в дереве от ресурсов, от которых проект отказался.\n *\n * Отказ значит «больше не клади», а не «убери»: разложенное коммитится, рядом с правилом\n * лежит написанный проектом компаньон, и стирать это молча пакет не вправе. Но и молчать\n * нельзя — брошенный файл читается как действующее правило, и агент по нему работает.\n */\n readonly abandoned: readonly string[];\n /**\n * Ресурсы, у которых виды есть, а вида под выбор дерева нет.\n *\n * Пропустить такой ресурс молча значит разложить правило и не разложить инструмент, который\n * оно зовёт: дерево получает указание без исполнителя и узнаёт об этом, когда команда из\n * правила не находится. Поэтому непустой список — отказ раскладки наравне с дыркой без\n * значения и с файлом, который правили руками.\n */\n readonly gaps: readonly IGapOfVariant[];\n /**\n * Разложенные гарды, которых нет в настройке агента: файл лежит, а позвать его некому.\n *\n * Раскладку это не отбивает — настройка принадлежит дереву, и пакет в неё не пишет, — но и\n * молчать нельзя: гард, который не зовут, неотличим от гарда, который всё пропускает.\n */\n readonly unbound: readonly IHookBinding[];\n readonly written: readonly string[];\n}\n\n/** Шаблон черновика компаньона — тот же, что пакет кладёт проекту в шаблоны. */\nconst COMPANION_TEMPLATE: string = 'templates/implementation.md';\n\nconst read: (path: string) => string | null = (path: string): string | null => (existsSync(path) ? readFileSync(path, 'utf8') : null);\n\n/**\n * Тело ресурса: текст пакета, поверх него надстройка проекта, и уже потом подстановка значений.\n *\n * Порядок именно такой. Надстройка тоже пишется с дырками — иначе проект, дописавший раздел про\n * свою главную ветку, зашил бы её имя в двух местах: в конфиге и в тексте.\n */\nfunction renderAsset(asset: IAsset, config: IConfig, root: string): IRenderResult {\n const override: string | null = read(join(root, OVERRIDES_DIR, asset.id));\n const merged: string = override ? renderDocument(mergeDocuments(parseDocument(asset.text), parseDocument(override))) : asset.text;\n\n return renderVars(merged, config.vars);\n}\n\n/**\n * Что лежит в дереве от ресурсов, которые проект больше не берёт.\n *\n * Ищется по тем же правилам раскладки, что и всё остальное: путь ресурса вычисляется так, будто\n * его кладут, и проверяется, лежит ли там файл с шапкой пакета. Чужой файл на том же пути\n * брошенным не считается — его пакет не клал.\n */\nfunction abandonedOf(config: IConfig, root: string, assetsDir: string): readonly string[] {\n const taken: ReadonlySet<string> = new Set(collectAssets(config, assetsDir).map((asset: IAsset): string => asset.id));\n\n return readCatalog(assetsDir)\n .filter((entry: IEntryOfCatalog): boolean => !taken.has(entry.id) && matchesVariant(entry.variant, config.variants))\n .map((entry: IEntryOfCatalog): string => targetOf(entry, config.layout))\n .filter((path: string): boolean => {\n const existing: string | null = read(join(root, path));\n\n return existing !== null && readStamped(existing) !== null;\n });\n}\n\n/**\n * Что о себе говорят гарды, которые дерево берёт. Читается у ресурсов, а не у разложенных\n * файлов: карта нужна и до первой раскладки — чтобы было что вставить в настройку.\n */\nfunction bindingsOf(config: IConfig, assetsDir: string): readonly IHookBinding[] {\n const bindings: IHookBinding[] = [];\n for (const asset of collectAssets(config, assetsDir)) {\n if (asset.kind !== 'hooks') {\n continue;\n }\n bindings.push(...declaredIn(asset.text, asset.target));\n }\n\n return bindings;\n}\n\nexport function planSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const planned: IPlanned[] = [];\n const missing: Map<string, readonly string[]> = new Map();\n const companions: ICompanion[] = [];\n const template: string | null = read(join(assetsDir, COMPANION_TEMPLATE));\n\n for (const asset of collectAssets(config, assetsDir)) {\n const rendered: IRenderResult = renderAsset(asset, config, root);\n if (rendered.missing.length) {\n missing.set(asset.id, rendered.missing);\n continue;\n }\n planned.push(\n planFile({ path: asset.target, asset: asset.id, version, rendered: rendered.text, existing: read(join(root, asset.target)) })\n );\n if (asset.kind === 'rules' && template !== null) {\n companions.push(planCompanion(asset, read(join(root, pathOf(asset))), template));\n }\n }\n\n return {\n planned,\n missing,\n companions,\n abandoned: abandonedOf(config, root, assetsDir),\n gaps: variantGaps(readCatalog(assetsDir), config),\n unbound: unboundHooks(bindingsOf(config, assetsDir), root),\n written: [],\n };\n}\n\n/** Раскладка. Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого. */\nexport function runSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult {\n const result: ISyncResult = planSync(config, root, version, assetsDir);\n if (result.missing.size || result.gaps.length || result.planned.some((entry: IPlanned): boolean => isRefusal(entry.outcome))) {\n return result;\n }\n\n // Право на запуск переносится с файла в пакете. Без него гард отвечает отказом доступа, то\n // есть ненулевым кодом, а ненулевой код у гарда значит «правка отбита»: разложенный набор\n // отбивал бы подряд всё, включая сборку и тесты, и причина при этом нигде не называлась.\n const executable: ReadonlySet<string> = new Set(\n collectAssets(config, assetsDir)\n .filter((asset: IAsset): boolean => asset.executable)\n .map((asset: IAsset): string => asset.id)\n );\n\n const written: string[] = [];\n for (const entry of result.planned) {\n if (entry.content === null) {\n continue;\n }\n const path: string = join(root, entry.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, entry.content, 'utf8');\n if (executable.has(entry.asset)) {\n chmodSync(path, 0o755);\n }\n written.push(entry.path);\n }\n\n // Черновик компаньона кладётся только там, где файла нет вовсе. Он без шапки и без суммы:\n // сверять в нём нечего — с первой правки проекта это его текст, а не текст пакета.\n for (const companion of result.companions) {\n if (companion.content === null) {\n continue;\n }\n const path: string = join(root, companion.path);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, companion.content, 'utf8');\n written.push(companion.path);\n }\n\n return { ...result, written };\n}\n\nexport const pendingOf: (result: ISyncResult) => readonly IPlanned[] = (result: ISyncResult): readonly IPlanned[] =>\n result.planned.filter((entry: IPlanned): boolean => isPending(entry.outcome));\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/agent-kit",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Переносимый слой правил для агента: законы, хуки, проверки и агенты, раскладываемые в репозиторий одной командой",
5
5
  "author": "RT Team",
6
6
  "license": "Apache-2.0",
Binary file
Binary file