@rt-tools/agent-kit 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +15 -1
  2. package/assets/checks/check-push-gate.mjs +139 -0
  3. package/assets/checks/check-reuse.mjs +32 -98
  4. package/assets/checks/rt-kit-checks.config.mjs +19 -0
  5. package/assets/checks/signals/angular.json +19 -0
  6. package/assets/checks/signals/core.json +19 -0
  7. package/assets/checks/signals/store.json +14 -0
  8. package/assets/checks/signals/ui-kit-v2.json +48 -0
  9. package/assets/checks/signals/ui-kit.json +11 -0
  10. package/assets/checks/signals/utils.json +15 -0
  11. package/assets/checks/signals.mjs +71 -0
  12. package/assets/defaults/project.sh +9 -7
  13. package/assets/hooks/reuse-first-guard.sh +90 -1
  14. package/assets/patterns/git-workflow-commit.azure.md +18 -0
  15. package/assets/patterns/git-workflow-commit.github.md +18 -0
  16. package/assets/patterns/git-workflow-commit.gitlab.md +18 -0
  17. package/assets/patterns/reuse-first-extend.md +30 -0
  18. package/assets/patterns/spec-driven-domain.md +2 -2
  19. package/assets/rules/git-workflow.azure.md +15 -0
  20. package/assets/rules/git-workflow.github.md +15 -0
  21. package/assets/rules/git-workflow.gitlab.md +15 -0
  22. package/assets/rules/reuse-first.md +31 -13
  23. package/assets/skills/agent-kit.md +13 -8
  24. package/lib/assets.d.ts +1 -1
  25. package/lib/assets.d.ts.map +1 -1
  26. package/lib/assets.js +2 -4
  27. package/lib/assets.js.map +1 -1
  28. package/lib/catalog.d.ts +73 -0
  29. package/lib/catalog.d.ts.map +1 -1
  30. package/lib/catalog.js +121 -0
  31. package/lib/catalog.js.map +1 -1
  32. package/lib/commands.d.ts.map +1 -1
  33. package/lib/commands.js +84 -5
  34. package/lib/commands.js.map +1 -1
  35. package/lib/integrity.d.ts +25 -12
  36. package/lib/integrity.d.ts.map +1 -1
  37. package/lib/integrity.js +40 -18
  38. package/lib/integrity.js.map +1 -1
  39. package/lib/proposals.d.ts +9 -1
  40. package/lib/proposals.d.ts.map +1 -1
  41. package/lib/proposals.js +11 -2
  42. package/lib/proposals.js.map +1 -1
  43. package/lib/retired.d.ts +30 -0
  44. package/lib/retired.d.ts.map +1 -0
  45. package/lib/retired.js +19 -0
  46. package/lib/retired.js.map +1 -0
  47. package/lib/sync.d.ts +45 -1
  48. package/lib/sync.d.ts.map +1 -1
  49. package/lib/sync.js +44 -10
  50. package/lib/sync.js.map +1 -1
  51. package/package.json +1 -1
  52. package/rt-tools-agent-kit-0.8.0.tgz +0 -0
  53. package/assets/laws/application/money.md +0 -41
  54. package/assets/laws/application/ownership.md +0 -32
  55. package/assets/patterns/ownership-scope-resolve.md +0 -69
  56. package/assets/patterns/ownership-session-procedure.md +0 -102
  57. package/assets/patterns/pricing-quote.md +0 -71
  58. package/assets/rules/ownership-scope.md +0 -66
  59. package/assets/rules/ownership-session.md +0 -124
  60. package/assets/rules/pricing.md +0 -68
  61. package/rt-tools-agent-kit-0.6.0.tgz +0 -0
package/lib/proposals.js CHANGED
@@ -100,12 +100,21 @@ export function marksOf(root, remote) {
100
100
  const name = basename(root);
101
101
  return [root, name, remote].filter(Boolean);
102
102
  }
103
- /** Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько. */
103
+ /**
104
+ * Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько.
105
+ *
106
+ * Место считается от номера строки заголовка — но номера у всех блоков посчитаны при разборе, до
107
+ * единой вставки, а каждая вставка сдвигает файл на строку вниз. Поэтому к номеру прибавляется
108
+ * число пометок, уже стоящих выше: без этого вторая пометка встаёт на строку выше своего места,
109
+ * пятая — на четыре, то есть посреди готового текста правки, а последнему блоку её не достаётся
110
+ * вовсе, и при следующей отправке он уезжает вторым разом.
111
+ */
104
112
  export function markSent(text, proposal, url) {
105
113
  const lines = text.split('\n');
114
+ const shift = lines.slice(0, proposal.line).filter((line) => SENT.test(line)).length;
106
115
  // Пометка встаёт сразу под заголовок: конец блока определяется следующим заголовком, а его
107
116
  // может и не быть — тогда «конец» пришлось бы искать по пустым строкам в хвосте файла.
108
- lines.splice(proposal.line, 0, `- **отправлено:** ${url}`);
117
+ lines.splice(proposal.line + shift, 0, `- **отправлено:** ${url}`);
109
118
  return lines.join('\n');
110
119
  }
111
120
  //# sourceMappingURL=proposals.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"proposals.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/proposals.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Предложения по слою правил: разбор файла, отбор того, что уезжает наружу, и проверка на адрес\n * дерева.\n *\n * Файл пишет главный агент после разбора закрытой задачи — по блоку на предложение, с адресом в\n * заголовке. Адрес роль проставляет и так; здесь он впервые становится тем, что читает машина.\n *\n * Проверка на утечку живёт в этом же модуле не для удобства: предложение уезжает в чужой\n * репозиторий целиком, и запрет называть чужое дерево обязан держаться проверкой, а не памятью\n * того, кто пишет.\n */\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\n/** Куда главный агент кладёт предложения. Путь от корня дерева. */\nexport const PROPOSALS_DIR: string = '.claude/rt-kit/proposals';\n\n/** Адрес предложения: куда правка идёт. Наружу уезжает только первый. */\nexport const TO_PACKAGE: string = 'пакет';\nexport const TO_COMPANION: string = 'компаньон';\nexport const TO_TREE: string = 'дерево';\n\nconst ADDRESSES: readonly string[] = [TO_PACKAGE, TO_COMPANION, TO_TREE];\n\n/** Заголовок блока: `## <адрес> · <ресурс>`. Разделитель — тот же, что в шаблоне. */\nconst HEADING: RegExp = /^##\\s+(\\S+)\\s+·\\s+(\\S.*?)\\s*$/;\n\n/** Пометка об отправке. По ней же предложение узнаётся отправленным. */\nconst SENT: RegExp = /^-\\s+\\*\\*отправлено:\\*\\*\\s*(\\S+)/m;\n\nexport interface IProposal {\n readonly address: string;\n readonly resource: string;\n /** Тело блока без заголовка: место, повод и готовый текст. */\n readonly body: string;\n /** Ссылка на заведённую запись; пусто — не отправлялось. */\n readonly sent: string;\n /** Файл, в котором блок лежит, путём от корня дерева. */\n readonly file: string;\n /** Строка заголовка в файле, считая с единицы. */\n readonly line: number;\n}\n\n/**\n * Блоки файла предложений.\n *\n * Незаполненный образец пропускается: в шаблоне ресурс стоит скобками — `rules/<правило>.md`, —\n * и отправить его значило бы завести запись о правке несуществующего файла.\n */\nexport function parseProposals(text: string, file: string): readonly IProposal[] {\n const lines: readonly string[] = text.split('\\n');\n const found: IProposal[] = [];\n let at: number = -1;\n\n const close: (end: number) => void = (end: number): void => {\n if (at < 0) {\n return;\n }\n const heading: RegExpExecArray = HEADING.exec(lines[at]) as RegExpExecArray;\n const body: string = lines\n .slice(at + 1, end)\n .join('\\n')\n .trim();\n const address: string = heading[1];\n const resource: string = heading[2];\n if (ADDRESSES.includes(address) && !resource.includes('<')) {\n found.push({ address, resource, body, sent: SENT.exec(body)?.[1] ?? '', file, line: at + 1 });\n }\n at = -1;\n };\n\n lines.forEach((line: string, index: number): void => {\n if (line.startsWith('## ')) {\n close(index);\n if (HEADING.test(line)) {\n at = index;\n }\n }\n });\n close(lines.length);\n\n return found;\n}\n\n/** Все файлы предложений дерева, в порядке имён. */\nexport function readProposals(root: string): readonly IProposal[] {\n const dir: string = join(root, PROPOSALS_DIR);\n if (!existsSync(dir)) {\n return [];\n }\n\n return readdirSync(dir)\n .filter((name: string): boolean => name.endsWith('.md'))\n .sort()\n .flatMap((name: string): readonly IProposal[] => parseProposals(readFileSync(join(dir, name), 'utf8'), join(PROPOSALS_DIR, name)));\n}\n\nexport interface ILeak {\n /** Строка блока, считая с единицы от заголовка. */\n readonly line: number;\n readonly text: string;\n readonly why: string;\n}\n\n/** Абсолютный путь машины: он не бывает общим ни для одного дерева, кроме этого. */\nconst ABSOLUTE: RegExp = /(^|[\\s(`'\"])(~\\/|\\/(Users|home|var|opt|srv|mnt)\\/)\\S+/;\n\n/**\n * Что в тексте называет дерево, из которого предложение уезжает.\n *\n * `marks` — имена, которые знает только это дерево: корень, его последнее звено, адрес его\n * удалённого репозитория. Пакет их не угадывает — они приходят доводом с края.\n */\nexport function leaksIn(text: string, marks: readonly string[]): readonly ILeak[] {\n const found: ILeak[] = [];\n\n text.split('\\n').forEach((line: string, index: number): void => {\n if (ABSOLUTE.test(line)) {\n found.push({ line: index + 1, text: line.trim(), why: 'абсолютный путь этой машины' });\n\n return;\n }\n const mark: string | undefined = marks.find((value: string): boolean => Boolean(value) && line.includes(value));\n if (mark) {\n found.push({ line: index + 1, text: line.trim(), why: `имя этого дерева: ${mark}` });\n }\n });\n\n return found;\n}\n\n/**\n * Чем дерево себя выдаёт. Последнее звено пути берётся отдельно от полного: в тексте оно\n * встречается чаще, чем путь целиком.\n */\nexport function marksOf(root: string, remote: string): readonly string[] {\n const name: string = basename(root);\n\n return [root, name, remote].filter(Boolean);\n}\n\n/** Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько. */\nexport function markSent(text: string, proposal: IProposal, url: string): string {\n const lines: string[] = text.split('\\n');\n // Пометка встаёт сразу под заголовок: конец блока определяется следующим заголовком, а его\n // может и не быть — тогда «конец» пришлось бы искать по пустым строкам в хвосте файла.\n lines.splice(proposal.line, 0, `- **отправлено:** ${url}`);\n\n return lines.join('\\n');\n}\n"]}
1
+ {"version":3,"file":"proposals.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/proposals.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Предложения по слою правил: разбор файла, отбор того, что уезжает наружу, и проверка на адрес\n * дерева.\n *\n * Файл пишет главный агент после разбора закрытой задачи — по блоку на предложение, с адресом в\n * заголовке. Адрес роль проставляет и так; здесь он впервые становится тем, что читает машина.\n *\n * Проверка на утечку живёт в этом же модуле не для удобства: предложение уезжает в чужой\n * репозиторий целиком, и запрет называть чужое дерево обязан держаться проверкой, а не памятью\n * того, кто пишет.\n */\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\n/** Куда главный агент кладёт предложения. Путь от корня дерева. */\nexport const PROPOSALS_DIR: string = '.claude/rt-kit/proposals';\n\n/** Адрес предложения: куда правка идёт. Наружу уезжает только первый. */\nexport const TO_PACKAGE: string = 'пакет';\nexport const TO_COMPANION: string = 'компаньон';\nexport const TO_TREE: string = 'дерево';\n\nconst ADDRESSES: readonly string[] = [TO_PACKAGE, TO_COMPANION, TO_TREE];\n\n/** Заголовок блока: `## <адрес> · <ресурс>`. Разделитель — тот же, что в шаблоне. */\nconst HEADING: RegExp = /^##\\s+(\\S+)\\s+·\\s+(\\S.*?)\\s*$/;\n\n/** Пометка об отправке. По ней же предложение узнаётся отправленным. */\nconst SENT: RegExp = /^-\\s+\\*\\*отправлено:\\*\\*\\s*(\\S+)/m;\n\nexport interface IProposal {\n readonly address: string;\n readonly resource: string;\n /** Тело блока без заголовка: место, повод и готовый текст. */\n readonly body: string;\n /** Ссылка на заведённую запись; пусто — не отправлялось. */\n readonly sent: string;\n /** Файл, в котором блок лежит, путём от корня дерева. */\n readonly file: string;\n /** Строка заголовка в файле, считая с единицы. */\n readonly line: number;\n}\n\n/**\n * Блоки файла предложений.\n *\n * Незаполненный образец пропускается: в шаблоне ресурс стоит скобками — `rules/<правило>.md`, —\n * и отправить его значило бы завести запись о правке несуществующего файла.\n */\nexport function parseProposals(text: string, file: string): readonly IProposal[] {\n const lines: readonly string[] = text.split('\\n');\n const found: IProposal[] = [];\n let at: number = -1;\n\n const close: (end: number) => void = (end: number): void => {\n if (at < 0) {\n return;\n }\n const heading: RegExpExecArray = HEADING.exec(lines[at]) as RegExpExecArray;\n const body: string = lines\n .slice(at + 1, end)\n .join('\\n')\n .trim();\n const address: string = heading[1];\n const resource: string = heading[2];\n if (ADDRESSES.includes(address) && !resource.includes('<')) {\n found.push({ address, resource, body, sent: SENT.exec(body)?.[1] ?? '', file, line: at + 1 });\n }\n at = -1;\n };\n\n lines.forEach((line: string, index: number): void => {\n if (line.startsWith('## ')) {\n close(index);\n if (HEADING.test(line)) {\n at = index;\n }\n }\n });\n close(lines.length);\n\n return found;\n}\n\n/** Все файлы предложений дерева, в порядке имён. */\nexport function readProposals(root: string): readonly IProposal[] {\n const dir: string = join(root, PROPOSALS_DIR);\n if (!existsSync(dir)) {\n return [];\n }\n\n return readdirSync(dir)\n .filter((name: string): boolean => name.endsWith('.md'))\n .sort()\n .flatMap((name: string): readonly IProposal[] => parseProposals(readFileSync(join(dir, name), 'utf8'), join(PROPOSALS_DIR, name)));\n}\n\nexport interface ILeak {\n /** Строка блока, считая с единицы от заголовка. */\n readonly line: number;\n readonly text: string;\n readonly why: string;\n}\n\n/** Абсолютный путь машины: он не бывает общим ни для одного дерева, кроме этого. */\nconst ABSOLUTE: RegExp = /(^|[\\s(`'\"])(~\\/|\\/(Users|home|var|opt|srv|mnt)\\/)\\S+/;\n\n/**\n * Что в тексте называет дерево, из которого предложение уезжает.\n *\n * `marks` — имена, которые знает только это дерево: корень, его последнее звено, адрес его\n * удалённого репозитория. Пакет их не угадывает — они приходят доводом с края.\n */\nexport function leaksIn(text: string, marks: readonly string[]): readonly ILeak[] {\n const found: ILeak[] = [];\n\n text.split('\\n').forEach((line: string, index: number): void => {\n if (ABSOLUTE.test(line)) {\n found.push({ line: index + 1, text: line.trim(), why: 'абсолютный путь этой машины' });\n\n return;\n }\n const mark: string | undefined = marks.find((value: string): boolean => Boolean(value) && line.includes(value));\n if (mark) {\n found.push({ line: index + 1, text: line.trim(), why: `имя этого дерева: ${mark}` });\n }\n });\n\n return found;\n}\n\n/**\n * Чем дерево себя выдаёт. Последнее звено пути берётся отдельно от полного: в тексте оно\n * встречается чаще, чем путь целиком.\n */\nexport function marksOf(root: string, remote: string): readonly string[] {\n const name: string = basename(root);\n\n return [root, name, remote].filter(Boolean);\n}\n\n/**\n * Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько.\n *\n * Место считается от номера строки заголовка — но номера у всех блоков посчитаны при разборе, до\n * единой вставки, а каждая вставка сдвигает файл на строку вниз. Поэтому к номеру прибавляется\n * число пометок, уже стоящих выше: без этого вторая пометка встаёт на строку выше своего места,\n * пятая — на четыре, то есть посреди готового текста правки, а последнему блоку её не достаётся\n * вовсе, и при следующей отправке он уезжает вторым разом.\n */\nexport function markSent(text: string, proposal: IProposal, url: string): string {\n const lines: string[] = text.split('\\n');\n const shift: number = lines.slice(0, proposal.line).filter((line: string): boolean => SENT.test(line)).length;\n // Пометка встаёт сразу под заголовок: конец блока определяется следующим заголовком, а его\n // может и не быть — тогда «конец» пришлось бы искать по пустым строкам в хвосте файла.\n lines.splice(proposal.line + shift, 0, `- **отправлено:** ${url}`);\n\n return lines.join('\\n');\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Имена, ушедшие из набора.
3
+ *
4
+ * Раскладка ищет брошенный файл по каталогу пакета — а снятого ресурса в каталоге нет, и файл,
5
+ * положенный им в дерево прошлой редакцией, не назовёт никто. Он остаётся лежать с шапкой пакета,
6
+ * никогда не обновляется и читается агентом как действующее правило: гейт правил требует его по
7
+ * имени, компаньон при нём продолжает проверяться, а сверка раскладки зелена — она судит по
8
+ * выбранному, а выбрать снятое уже нельзя.
9
+ *
10
+ * Поэтому имя переживает свой файл: список называет его один раз при раскладке, а убирает файл
11
+ * дерево само — в чужие файлы пакет не пишет никогда.
12
+ */
13
+ import { TKind } from './config.js';
14
+ export interface IRetired {
15
+ /** Идентификатор, каким ресурс звался в наборе: `rules/pricing.md`. */
16
+ readonly id: string;
17
+ readonly kind: TKind;
18
+ /** Имя без рода и расширения — им ресурс ложился в дерево. */
19
+ readonly name: string;
20
+ /** Редакция пакета, в которой ресурс снят. */
21
+ readonly since: string;
22
+ /** Почему снят: строка едет читателю как есть. */
23
+ readonly why: string;
24
+ }
25
+ /**
26
+ * Список ведётся руками вместе со снятием файла: выведенный из истории системы контроля версий,
27
+ * он был бы верен только там, где она есть, а пакет ставится из реестра.
28
+ */
29
+ export declare const RETIRED: readonly IRetired[];
30
+ //# sourceMappingURL=retired.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retired.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/retired.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,MAAM,WAAW,QAAQ;IACrB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACxB;AAOD;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,SAAS,QAAQ,EAStC,CAAC"}
package/lib/retired.js ADDED
@@ -0,0 +1,19 @@
1
+ const DOMAIN_LAW = 'закон называл предметы одного приложения и в чужом дереве неисполним';
2
+ const AT_MONEY = 'правило при снятом законе о деньгах';
3
+ const AT_OWNERSHIP = 'правило при снятом законе о владеющей сущности';
4
+ const AT_RULE = 'паттерн при снятом правиле';
5
+ /**
6
+ * Список ведётся руками вместе со снятием файла: выведенный из истории системы контроля версий,
7
+ * он был бы верен только там, где она есть, а пакет ставится из реестра.
8
+ */
9
+ export const RETIRED = [
10
+ { id: 'laws/application/money.md', kind: 'laws', name: 'application/money', since: '0.7.0', why: DOMAIN_LAW },
11
+ { id: 'laws/application/ownership.md', kind: 'laws', name: 'application/ownership', since: '0.7.0', why: DOMAIN_LAW },
12
+ { id: 'rules/pricing.md', kind: 'rules', name: 'pricing', since: '0.7.0', why: AT_MONEY },
13
+ { id: 'rules/ownership-scope.md', kind: 'rules', name: 'ownership-scope', since: '0.7.0', why: AT_OWNERSHIP },
14
+ { id: 'rules/ownership-session.md', kind: 'rules', name: 'ownership-session', since: '0.7.0', why: AT_OWNERSHIP },
15
+ { id: 'patterns/pricing-quote.md', kind: 'patterns', name: 'pricing-quote', since: '0.7.0', why: AT_RULE },
16
+ { id: 'patterns/ownership-scope-resolve.md', kind: 'patterns', name: 'ownership-scope-resolve', since: '0.7.0', why: AT_RULE },
17
+ { id: 'patterns/ownership-session-procedure.md', kind: 'patterns', name: 'ownership-session-procedure', since: '0.7.0', why: AT_RULE },
18
+ ];
19
+ //# sourceMappingURL=retired.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retired.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/retired.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,GAAW,sEAAsE,CAAC;AAClG,MAAM,QAAQ,GAAW,qCAAqC,CAAC;AAC/D,MAAM,YAAY,GAAW,gDAAgD,CAAC;AAC9E,MAAM,OAAO,GAAW,4BAA4B,CAAC;AAErD;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAwB;IACxC,EAAE,EAAE,EAAE,2BAA2B,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE;IAC7G,EAAE,EAAE,EAAE,+BAA+B,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE;IACrH,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE;IACzF,EAAE,EAAE,EAAE,0BAA0B,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE;IAC7G,EAAE,EAAE,EAAE,4BAA4B,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE;IACjH,EAAE,EAAE,EAAE,2BAA2B,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;IAC1G,EAAE,EAAE,EAAE,qCAAqC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,yBAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;IAC9H,EAAE,EAAE,EAAE,yCAAyC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,6BAA6B,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;CACzI,CAAC","sourcesContent":["/**\n * Имена, ушедшие из набора.\n *\n * Раскладка ищет брошенный файл по каталогу пакета — а снятого ресурса в каталоге нет, и файл,\n * положенный им в дерево прошлой редакцией, не назовёт никто. Он остаётся лежать с шапкой пакета,\n * никогда не обновляется и читается агентом как действующее правило: гейт правил требует его по\n * имени, компаньон при нём продолжает проверяться, а сверка раскладки зелена — она судит по\n * выбранному, а выбрать снятое уже нельзя.\n *\n * Поэтому имя переживает свой файл: список называет его один раз при раскладке, а убирает файл\n * дерево само — в чужие файлы пакет не пишет никогда.\n */\nimport { TKind } from './config.js';\n\nexport interface IRetired {\n /** Идентификатор, каким ресурс звался в наборе: `rules/pricing.md`. */\n readonly id: string;\n readonly kind: TKind;\n /** Имя без рода и расширения — им ресурс ложился в дерево. */\n readonly name: string;\n /** Редакция пакета, в которой ресурс снят. */\n readonly since: string;\n /** Почему снят: строка едет читателю как есть. */\n readonly why: string;\n}\n\nconst DOMAIN_LAW: string = 'закон называл предметы одного приложения и в чужом дереве неисполним';\nconst AT_MONEY: string = 'правило при снятом законе о деньгах';\nconst AT_OWNERSHIP: string = 'правило при снятом законе о владеющей сущности';\nconst AT_RULE: string = 'паттерн при снятом правиле';\n\n/**\n * Список ведётся руками вместе со снятием файла: выведенный из истории системы контроля версий,\n * он был бы верен только там, где она есть, а пакет ставится из реестра.\n */\nexport const RETIRED: readonly IRetired[] = [\n { id: 'laws/application/money.md', kind: 'laws', name: 'application/money', since: '0.7.0', why: DOMAIN_LAW },\n { id: 'laws/application/ownership.md', kind: 'laws', name: 'application/ownership', since: '0.7.0', why: DOMAIN_LAW },\n { id: 'rules/pricing.md', kind: 'rules', name: 'pricing', since: '0.7.0', why: AT_MONEY },\n { id: 'rules/ownership-scope.md', kind: 'rules', name: 'ownership-scope', since: '0.7.0', why: AT_OWNERSHIP },\n { id: 'rules/ownership-session.md', kind: 'rules', name: 'ownership-session', since: '0.7.0', why: AT_OWNERSHIP },\n { id: 'patterns/pricing-quote.md', kind: 'patterns', name: 'pricing-quote', since: '0.7.0', why: AT_RULE },\n { id: 'patterns/ownership-scope-resolve.md', kind: 'patterns', name: 'ownership-scope-resolve', since: '0.7.0', why: AT_RULE },\n { id: 'patterns/ownership-session-procedure.md', kind: 'patterns', name: 'ownership-session-procedure', since: '0.7.0', why: AT_RULE },\n];\n"]}
package/lib/sync.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IBrokenLink, IGapOfVariant } from './catalog.js';
1
+ import { IBrokenLink, ICascadeCut, IGapOfVariant, IIdleSkip } from './catalog.js';
2
2
  import { ICompanion } from './companion.js';
3
3
  import { IConfig } from './config.js';
4
4
  import { IHookBinding } from './hooks-map.js';
@@ -41,8 +41,52 @@ export interface ISyncResult {
41
41
  * подмножестве, сколько бы связок ни было разорвано.
42
42
  */
43
43
  readonly broken: readonly IBrokenLink[];
44
+ /**
45
+ * Строки отказа, которые ничего не снимают.
46
+ *
47
+ * Предупреждение, а не отказ: строка становится лишней сама, обновлением пакета, без единой
48
+ * правки в дереве. Молчание же оставляет её в настройке навсегда — ровно так список отказа и
49
+ * дорастает до полусотни строк, ни одна из которых ни на что не влияет.
50
+ */
51
+ readonly idle: readonly IIdleSkip[];
52
+ /**
53
+ * Разложенные раньше файлы ресурсов, снятых теперь каскадом.
54
+ *
55
+ * Названы отдельно от брошенных: брошенное дерево отвергло само и знает об этом, а снятое
56
+ * каскадом ушло вслед за родителем — искать причину в отказе читатель пойдёт зря.
57
+ */
58
+ readonly cutOnDisk: readonly ICutFound[];
59
+ /**
60
+ * Ресурсы, названные выбором поимённо и снятые каскадом: дерево просило их прямо.
61
+ *
62
+ * Предупреждение, а не отказ: выбор с потомком при невыбранном родителе — состояние
63
+ * настройки, а не промах раскладки, и чинит его дерево у себя.
64
+ */
65
+ readonly namedCut: readonly ICascadeCut[];
66
+ /** Файлы ресурсов, которых в наборе больше нет: их убирает дерево, пакет только называет. */
67
+ readonly retired: readonly IRetiredFound[];
44
68
  readonly written: readonly string[];
45
69
  }
70
+ /** Разложенный раньше файл ресурса, снятого теперь каскадом, и причина, по которой он снят. */
71
+ export interface ICutFound {
72
+ /** Путь в дереве, где лежит файл. */
73
+ readonly path: string;
74
+ readonly cut: ICascadeCut;
75
+ }
76
+ /**
77
+ * Что лежит в дереве от ресурсов, которых в наборе больше нет.
78
+ *
79
+ * Брошенным такой файл не назовёт никто: брошенное ищется по каталогу пакета, а снятого в
80
+ * каталоге нет вовсе. Поэтому его ищут по списку снятых имён — там же, где записано, почему
81
+ * ресурс ушёл.
82
+ */
83
+ export interface IRetiredFound {
84
+ /** Путь в дереве, где лежит файл снятого ресурса. */
85
+ readonly path: string;
86
+ /** Редакция пакета, в которой ресурс снят. */
87
+ readonly since: string;
88
+ readonly why: string;
89
+ }
46
90
  export declare function planSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult;
47
91
  /** Раскладка. Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого. */
48
92
  export declare function runSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult;
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,EAAe,WAAW,EAAmB,aAAa,EAA4B,MAAM,cAAc,CAAC;AAClH,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;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,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,CA8BvG;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,EAGH,WAAW,EACX,WAAW,EAGX,aAAa,EACb,SAAS,EAIZ,MAAM,cAAc,CAAC;AACtB,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;AAOrE,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;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,SAAS,EAAE,CAAC;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAoBD,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACtB,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;CAC7B;AA+CD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACxB;AAgCD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAoCvG;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
@@ -8,11 +8,12 @@
8
8
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { dirname, join } from 'node:path';
10
10
  import { collectAssets, targetOf } from './assets.js';
11
- import { brokenLinks, readCatalog, variantGaps } from './catalog.js';
11
+ import { brokenLinks, cascadeCuts, idleSkips, namedButCut, readCatalog, variantGaps, } from './catalog.js';
12
12
  import { pathOf, planCompanion } from './companion.js';
13
13
  import { OVERRIDES_DIR } from './config.js';
14
14
  import { bindingsOf as declaredIn, unboundHooks } from './hooks-map.js';
15
15
  import { isPending, isRefusal, planFile } from './plan.js';
16
+ import { RETIRED } from './retired.js';
16
17
  import { mergeDocuments, parseDocument, renderDocument } from './sections.js';
17
18
  import { readStamped } from './stamp.js';
18
19
  import { renderVars } from './vars.js';
@@ -32,21 +33,49 @@ function renderAsset(asset, config, root) {
32
33
  return renderVars(merged, config.vars);
33
34
  }
34
35
  /**
35
- * Что лежит в дереве от ресурсов, которые проект больше не берёт.
36
+ * Что лежит в дереве от ресурсов, которые больше не раскладываются.
36
37
  *
37
38
  * Ищется по тем же правилам раскладки, что и всё остальное: путь ресурса вычисляется так, будто
38
39
  * его кладут, и проверяется, лежит ли там файл с шапкой пакета. Чужой файл на том же пути
39
40
  * брошенным не считается — его пакет не клал.
41
+ *
42
+ * Снятое каскадом отделяется здесь же: дерево от него не отказывалось, и, увидев его среди
43
+ * брошенного, читатель пойдёт искать строку отказа, которой нет.
40
44
  */
41
- function abandonedOf(config, root, assetsDir) {
45
+ function leftOnDisk(config, root, assetsDir) {
42
46
  const taken = new Set(collectAssets(config, assetsDir).map((asset) => asset.id));
43
- return readCatalog(assetsDir)
44
- .filter((entry) => !taken.has(entry.id) && matchesVariant(entry.variant, config.variants))
45
- .map((entry) => targetOf(entry, config.layout))
46
- .filter((path) => {
47
+ const catalog = readCatalog(assetsDir);
48
+ const cuts = new Map(cascadeCuts(catalog, config).map((one) => [one.id, one]));
49
+ const abandoned = [];
50
+ const cut = [];
51
+ for (const entry of catalog) {
52
+ if (taken.has(entry.id) || !matchesVariant(entry.variant, config.variants)) {
53
+ continue;
54
+ }
55
+ const path = targetOf(entry, config.layout);
56
+ const existing = read(join(root, path));
57
+ if (existing === null || readStamped(existing) === null) {
58
+ continue;
59
+ }
60
+ const found = cuts.get(entry.id);
61
+ if (found) {
62
+ cut.push({ path, cut: found });
63
+ continue;
64
+ }
65
+ abandoned.push(path);
66
+ }
67
+ return { abandoned, cut };
68
+ }
69
+ function retiredOf(config, root) {
70
+ const found = [];
71
+ for (const entry of RETIRED) {
72
+ const path = targetOf(entry, config.layout);
47
73
  const existing = read(join(root, path));
48
- return existing !== null && readStamped(existing) !== null;
49
- });
74
+ if (existing !== null && readStamped(existing) !== null) {
75
+ found.push({ path, since: entry.since, why: entry.why });
76
+ }
77
+ }
78
+ return found;
50
79
  }
51
80
  /**
52
81
  * Что о себе говорят гарды, которые дерево берёт. Читается у ресурсов, а не у разложенных
@@ -78,14 +107,19 @@ export function planSync(config, root, version, assetsDir) {
78
107
  companions.push(planCompanion(asset, read(join(root, pathOf(asset))), template));
79
108
  }
80
109
  }
110
+ const left = leftOnDisk(config, root, assetsDir);
81
111
  return {
82
112
  planned,
83
113
  missing,
84
114
  companions,
85
- abandoned: abandonedOf(config, root, assetsDir),
115
+ abandoned: left.abandoned,
86
116
  gaps: variantGaps(readCatalog(assetsDir), config),
87
117
  unbound: unboundHooks(bindingsOf(config, assetsDir), root),
88
118
  broken: brokenLinks(readCatalog(assetsDir), config),
119
+ idle: idleSkips(readCatalog(assetsDir), config),
120
+ cutOnDisk: left.cut,
121
+ namedCut: namedButCut(readCatalog(assetsDir), config),
122
+ retired: retiredOf(config, root),
89
123
  written: [],
90
124
  };
91
125
  }
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":"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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 { brokenLinks, IBrokenLink, 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 /**\n * Выбранные ресурсы, чьи требования в дерево не поехали.\n *\n * Предупреждение, а не отказ: дерево вправе закрыть требование своим средством, но обязано\n * знать, что закрывает. Молчание же оставляет его исправным на вид — сверка зелена на любом\n * подмножестве, сколько бы связок ни было разорвано.\n */\n readonly broken: readonly IBrokenLink[];\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 broken: brokenLinks(readCatalog(assetsDir), config),\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 {\n brokenLinks,\n cascadeCuts,\n IBrokenLink,\n ICascadeCut,\n idleSkips,\n IEntryOfCatalog,\n IGapOfVariant,\n IIdleSkip,\n namedButCut,\n readCatalog,\n variantGaps,\n} 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 { RETIRED } from './retired.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 /**\n * Выбранные ресурсы, чьи требования в дерево не поехали.\n *\n * Предупреждение, а не отказ: дерево вправе закрыть требование своим средством, но обязано\n * знать, что закрывает. Молчание же оставляет его исправным на вид — сверка зелена на любом\n * подмножестве, сколько бы связок ни было разорвано.\n */\n readonly broken: readonly IBrokenLink[];\n /**\n * Строки отказа, которые ничего не снимают.\n *\n * Предупреждение, а не отказ: строка становится лишней сама, обновлением пакета, без единой\n * правки в дереве. Молчание же оставляет её в настройке навсегда — ровно так список отказа и\n * дорастает до полусотни строк, ни одна из которых ни на что не влияет.\n */\n readonly idle: readonly IIdleSkip[];\n /**\n * Разложенные раньше файлы ресурсов, снятых теперь каскадом.\n *\n * Названы отдельно от брошенных: брошенное дерево отвергло само и знает об этом, а снятое\n * каскадом ушло вслед за родителем — искать причину в отказе читатель пойдёт зря.\n */\n readonly cutOnDisk: readonly ICutFound[];\n /**\n * Ресурсы, названные выбором поимённо и снятые каскадом: дерево просило их прямо.\n *\n * Предупреждение, а не отказ: выбор с потомком при невыбранном родителе — состояние\n * настройки, а не промах раскладки, и чинит его дерево у себя.\n */\n readonly namedCut: readonly ICascadeCut[];\n /** Файлы ресурсов, которых в наборе больше нет: их убирает дерево, пакет только называет. */\n readonly retired: readonly IRetiredFound[];\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/** Разложенный раньше файл ресурса, снятого теперь каскадом, и причина, по которой он снят. */\nexport interface ICutFound {\n /** Путь в дереве, где лежит файл. */\n readonly path: string;\n readonly cut: ICascadeCut;\n}\n\n/** Что дерево держит от ресурсов, которые больше не раскладываются. */\ninterface ILeftOnDisk {\n readonly abandoned: readonly string[];\n readonly cut: readonly ICutFound[];\n}\n\n/**\n * Что лежит в дереве от ресурсов, которые больше не раскладываются.\n *\n * Ищется по тем же правилам раскладки, что и всё остальное: путь ресурса вычисляется так, будто\n * его кладут, и проверяется, лежит ли там файл с шапкой пакета. Чужой файл на том же пути\n * брошенным не считается — его пакет не клал.\n *\n * Снятое каскадом отделяется здесь же: дерево от него не отказывалось, и, увидев его среди\n * брошенного, читатель пойдёт искать строку отказа, которой нет.\n */\nfunction leftOnDisk(config: IConfig, root: string, assetsDir: string): ILeftOnDisk {\n const taken: ReadonlySet<string> = new Set(collectAssets(config, assetsDir).map((asset: IAsset): string => asset.id));\n const catalog: readonly IEntryOfCatalog[] = readCatalog(assetsDir);\n const cuts: Map<string, ICascadeCut> = new Map(\n cascadeCuts(catalog, config).map((one: ICascadeCut): [string, ICascadeCut] => [one.id, one])\n );\n\n const abandoned: string[] = [];\n const cut: ICutFound[] = [];\n for (const entry of catalog) {\n if (taken.has(entry.id) || !matchesVariant(entry.variant, config.variants)) {\n continue;\n }\n const path: string = targetOf(entry, config.layout);\n const existing: string | null = read(join(root, path));\n if (existing === null || readStamped(existing) === null) {\n continue;\n }\n const found: ICascadeCut | undefined = cuts.get(entry.id);\n if (found) {\n cut.push({ path, cut: found });\n continue;\n }\n abandoned.push(path);\n }\n\n return { abandoned, cut };\n}\n\n/**\n * Что лежит в дереве от ресурсов, которых в наборе больше нет.\n *\n * Брошенным такой файл не назовёт никто: брошенное ищется по каталогу пакета, а снятого в\n * каталоге нет вовсе. Поэтому его ищут по списку снятых имён — там же, где записано, почему\n * ресурс ушёл.\n */\nexport interface IRetiredFound {\n /** Путь в дереве, где лежит файл снятого ресурса. */\n readonly path: string;\n /** Редакция пакета, в которой ресурс снят. */\n readonly since: string;\n readonly why: string;\n}\n\nfunction retiredOf(config: IConfig, root: string): readonly IRetiredFound[] {\n const found: IRetiredFound[] = [];\n\n for (const entry of RETIRED) {\n const path: string = targetOf(entry, config.layout);\n const existing: string | null = read(join(root, path));\n if (existing !== null && readStamped(existing) !== null) {\n found.push({ path, since: entry.since, why: entry.why });\n }\n }\n\n return found;\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 const left: ILeftOnDisk = leftOnDisk(config, root, assetsDir);\n\n return {\n planned,\n missing,\n companions,\n abandoned: left.abandoned,\n gaps: variantGaps(readCatalog(assetsDir), config),\n unbound: unboundHooks(bindingsOf(config, assetsDir), root),\n broken: brokenLinks(readCatalog(assetsDir), config),\n idle: idleSkips(readCatalog(assetsDir), config),\n cutOnDisk: left.cut,\n namedCut: namedButCut(readCatalog(assetsDir), config),\n retired: retiredOf(config, 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.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Переносимый слой правил для агента: законы, хуки, проверки и агенты, раскладываемые в репозиторий одной командой",
5
5
  "author": "RT Team",
6
6
  "license": "Apache-2.0",
Binary file
@@ -1,41 +0,0 @@
1
- # Закон о деньгах
2
-
3
- Как в системе устроены суммы. Правила держат расчёт цены, заказы, письма, документы-основания
4
- и сводки одновременно, и разойтись им нельзя: сумма, названная в трёх местах по-разному, — это
5
- не три числа, а одно сломанное.
6
-
7
- ## Терминология
8
-
9
- | Термин | Определение |
10
- | --------------- | ------------------------------------------------------------------------------- |
11
- | Валюта хранения | Единственная валюта, в которой сумма записана и посчитана. Одна на всю систему |
12
- | Валюта показа | Валюта, в которой сумму видит пользователь. Может отличаться от валюты хранения |
13
- | Подытог | Сумма до скидок |
14
- | Итог | Сумма после применённой скидки — то, что называется ценой |
15
-
16
- ## Статьи
17
-
18
- - **Сумма хранится в целых единицах валюты хранения.** Дробная часть даёт расхождение между
19
- подытогом и итогом: то, что показано построчно, перестаёт складываться в то, что показано
20
- внизу. Если дробная часть в предметной области значима, единицей хранения становится она
21
- сама, а не доля от целого.
22
- - **Валюта хранения одна на систему, и она объявлена.** Две валюты хранения означают, что
23
- каждое сравнение сумм — это скрытый пересчёт по курсу, которого в момент сравнения может не
24
- быть вовсе.
25
- - **Округление происходит один раз — при расчёте цены.** Сумма на экране, сумма в письме и
26
- сумма в хранилище — одно и то же число, а не три результата одного пересчёта.
27
- - **Из подходящих скидок применяется одна — наибольшая.** Сложение дало бы суммы, которых
28
- никто не закладывал, и объяснить пользователю итог стало бы нечем.
29
- - **При равной выгоде побеждает та скидка, которую пользователь ввёл руками.** Он ждёт
30
- подтверждения своему действию, а «код не сработал» при той же итоговой цене читается как
31
- поломка.
32
- - **Скидка не уводит цену ниже нуля.** Скидка суммой ограничена самим подытогом.
33
- - **Пересчёт в чужую валюту не хранится.** Хранится сумма в валюте хранения и код валюты
34
- показа; само число вычисляется на показ. Сохранённое устарело бы вместе с курсом, а по виду
35
- оставалось бы точным.
36
- - **Оплата идёт в валюте хранения; остальные валюты — справка.** Ни подтверждение, ни документ
37
- не называют справочное число суммой к оплате.
38
- - **Валюта показа следует за пользователем.** Он выбрал её на экране — в письме сумма
39
- пересчитана в неё же. Письмо в другой валюте читается как другая цена.
40
- - **Курс недоступен — сумма показывается в валюте хранения без пересчёта.** Пересчёт по
41
- неизвестно какому курсу хуже отсутствующего: отсутствие видно, а неверный курс — нет.
@@ -1,32 +0,0 @@
1
- # Закон о владеющей сущности
2
-
3
- Как система ведёт себя, когда владеющих сущностей больше одной. Владеющая сущность — та, кому
4
- принадлежат записи остальных доменов: площадка, объект, организация, точка. Правило держат все
5
- эти домены сразу, и разойтись им нельзя.
6
-
7
- ## Терминология
8
-
9
- | Термин | Определение |
10
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
11
- | Владеющая сущность | Запись, которой принадлежат записи остальных доменов. Не настройка: у неё свой идентификатор, свои права и своя видимость |
12
- | Действующая | Та, что видна пользователю снаружи |
13
- | Скрытая | Заведённая, но снаружи не показанная. Её записи никуда не деваются |
14
- | Сводка | Экран, отвечающий сразу по всем действующим: очередь работ, статистика, дашборд |
15
-
16
- ## Статьи
17
-
18
- - **Запись принадлежит владеющей сущности, а не системе.** Настроек, действующих сразу на все,
19
- нет: заведение второй сущности не меняет поведение первой.
20
- - **Пустой идентификатор в запросе означает единственную действующую.** Пока она одна, клиент
21
- вправе её не называть — и не обязан меняться в тот день, когда появится вторая.
22
- - **При двух и более действующих запрос обязан назвать сущность.** Умолчание перестаёт быть
23
- однозначным, и запрос отбивается как неверный, а не выбирает первую попавшуюся.
24
- - **Названная явно находится, даже если она скрыта.** У скрытой остаются записи, деньги и
25
- переписка, и добраться до них надо как раз тогда, когда снаружи её уже нет.
26
- - **Там, где показана сводка, пустой идентификатор означает все действующие.** Очередь работ,
27
- статистика и дашборд отвечают по всему набору, а не требуют сперва выбрать одну.
28
- - **Когда действующих нет вовсе, запрос отбивается.** Скрытая вместо отсутствующей действующей
29
- не подставляется: показанное в этот момент выглядело бы работающим, а на деле показывало бы
30
- снятое с показа.
31
- - **Выбор сущности виден на экране и действует на всё, что говорит об одной.** Пользователь,
32
- который не видит, о какой сущности идёт речь, читает чужие числа как свои.
@@ -1,69 +0,0 @@
1
- ---
2
- name: ownership-scope-resolve
3
- kind: pattern
4
- rule: ownership-scope
5
- description: Паттерн правила ownership-scope. Брать, когда процедура принимает идентификатор владеющей сущности — готовое разрешение одной и всех действующих, коды отказа на каждый случай, условие отбора вместо своего поля запроса. Не брать для объявления доступа к процедуре — это паттерн permissions-procedure.
6
- ---
7
-
8
- # Разрешение владеющей сущности в процедуре
9
-
10
- Паттерн правила `ownership-scope`. Что при этом должно быть верно — закон
11
- `docs/constitution/application/ownership.md`.
12
-
13
- ## Когда брать
14
-
15
- - Процедура принимает идентификатор владеющей сущности.
16
- - Экран или сводка отвечает сразу по всем действующим.
17
- - Заводится новый домен, записи которого принадлежат владеющей сущности.
18
-
19
- ## Одна сущность
20
-
21
- ```typescript
22
- const ownerId: string = await resolveRequestedOwner(this.#db, request.ownerId);
23
- ```
24
-
25
- Обёртка сама разбирает случаи и переводит их в отказ:
26
-
27
- | Что пришло | Что происходит |
28
- | ----------------------------------------------------- | ------------------------- |
29
- | пусто, действующая одна | берётся она |
30
- | пусто, действующих две и больше | `Code.InvalidArgument` |
31
- | пусто, действующих нет вовсе | `Code.FailedPrecondition` |
32
- | названная явно, есть — хоть действующая, хоть скрытая | берётся она |
33
- | названная явно, такой нет | `Code.NotFound` |
34
-
35
- Своего разбора не заводить: чистая функция разбирает случай, обёртка ходит в хранилище. Вторая
36
- реализация того же правила однажды уже жила у обработчика статистики соседнего домена и успела
37
- разойтись порядком выборки.
38
-
39
- ## Все действующие
40
-
41
- Там, где показана сводка, пустой идентификатор означает все действующие:
42
-
43
- ```typescript
44
- const ownerIds: string[] = await resolveRequestedOwners(this.#db, request.ownerId);
45
- ```
46
-
47
- Так отвечают списки, статистика и дашборд.
48
-
49
- ## Отбор вместо своего поля запроса
50
-
51
- У списка, переведённого на общую выборку, сущность приходит условием отбора, а не отдельным
52
- полем: прежний номер поля в контракте помечен `reserved`.
53
-
54
- ```
55
- reserved 4;
56
- reserved "owner_id";
57
- ```
58
-
59
- ## Частые промахи
60
-
61
- - Свой разбор пустого идентификатора в обработчике: он разойдётся с общим при первой же правке.
62
- - Один и тот же код отказа на «действующих нет» и «такой сущности нет»: для вызывающего это
63
- разные случаи.
64
- - Подстановка скрытой, когда действующих нет: скрытая вместо отсутствующей действующей не
65
- берётся.
66
- - Общая настройка, действующая сразу на все: заведение второй не должно менять поведение
67
- первой.
68
- - Новое поле идентификатора рядом с выборкой у списка: отбор, живущий отдельно, не виден ни
69
- стору, ни адресу.