@rt-tools/agent-kit 0.8.2 → 0.9.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 (170) hide show
  1. package/README.md +13 -0
  2. package/assets/agents/rules-reviewer.md +83 -0
  3. package/assets/checks/board.github.mjs +48 -1
  4. package/assets/checks/check-board.github.mjs +84 -1
  5. package/assets/checks/check-dupes.mjs +66 -6
  6. package/assets/checks/check-lib-layers.mjs +13 -524
  7. package/assets/checks/check-specs.mjs +61 -747
  8. package/assets/checks/check-styles.mjs +185 -15
  9. package/assets/checks/lib-boundaries.mjs +143 -0
  10. package/assets/checks/lib-common.mjs +149 -0
  11. package/assets/checks/lib-domains.mjs +205 -0
  12. package/assets/checks/lib-manifests.mjs +60 -0
  13. package/assets/checks/lib-reexports.mjs +101 -0
  14. package/assets/checks/rt-kit-checks.config.mjs +26 -0
  15. package/assets/checks/spec-anchors.mjs +297 -0
  16. package/assets/checks/spec-common.mjs +222 -0
  17. package/assets/checks/spec-contract.mjs +152 -0
  18. package/assets/checks/spec-scenarios.mjs +201 -0
  19. package/assets/commands/feedback.md +95 -0
  20. package/assets/commands/rules-review.md +98 -0
  21. package/assets/commands/skill-curator.md +39 -22
  22. package/assets/defaults/project.sh +8 -0
  23. package/assets/docs/GLOSSARY.md +21 -20
  24. package/assets/hooks/git-guard-push-tests.sh +8 -4
  25. package/assets/hooks/reuse-first-guard.sh +16 -2
  26. package/assets/hooks/skill-gate.sh +1 -1
  27. package/assets/hooks/sql-guard-parse.sh +187 -0
  28. package/assets/hooks/sql-guard-request.sh +117 -0
  29. package/assets/hooks/sql-guard-target.sh +134 -0
  30. package/assets/hooks/sql-guard-write.sh +212 -0
  31. package/assets/hooks/sql-guard.sh +26 -596
  32. package/assets/hooks/task-flow-guard.sh +16 -2
  33. package/assets/hooks/waiting-turn-guard.sh +116 -0
  34. package/assets/laws/delivery.md +35 -0
  35. package/assets/laws/project-documentation.md +18 -0
  36. package/assets/laws/work-conduct.md +25 -0
  37. package/assets/patterns/admin-lists-screen.md +25 -14
  38. package/assets/patterns/admin-nav-item.md +1 -1
  39. package/assets/patterns/component-structure-new.md +1 -1
  40. package/assets/patterns/entity-aside.md +4 -2
  41. package/assets/patterns/git-workflow-commit.azure.md +74 -2
  42. package/assets/patterns/git-workflow-commit.github.md +75 -2
  43. package/assets/patterns/git-workflow-commit.gitlab.md +75 -4
  44. package/assets/patterns/git-workflow-docker.md +30 -0
  45. package/assets/patterns/observability-record.md +9 -0
  46. package/assets/patterns/shared-code-new.md +2 -2
  47. package/assets/patterns/task-flow-close.md +160 -47
  48. package/assets/patterns/task-flow-handoff.md +1 -1
  49. package/assets/patterns/task-flow-resume.md +3 -3
  50. package/assets/patterns/task-flow-start.md +32 -5
  51. package/assets/rules/angular-patterns.md +22 -0
  52. package/assets/rules/api-layer.md +25 -0
  53. package/assets/rules/browser-verification.md +32 -0
  54. package/assets/rules/component-structure.md +21 -0
  55. package/assets/rules/dependencies.md +22 -0
  56. package/assets/rules/doc-style.md +24 -0
  57. package/assets/rules/entity-conventions.needs-admin.md +21 -0
  58. package/assets/rules/entity-models.md +21 -0
  59. package/assets/rules/git-workflow.azure.md +57 -1
  60. package/assets/rules/git-workflow.github.md +89 -2
  61. package/assets/rules/git-workflow.gitlab.md +56 -1
  62. package/assets/rules/lib-layers.md +29 -0
  63. package/assets/rules/lists.md +37 -10
  64. package/assets/rules/navigation.md +21 -0
  65. package/assets/rules/observability.needs-app.md +23 -0
  66. package/assets/rules/permissions.md +23 -0
  67. package/assets/rules/platform-access.md +21 -0
  68. package/assets/rules/reuse-first.md +20 -0
  69. package/assets/rules/seo.md +19 -0
  70. package/assets/rules/shared-code.md +20 -1
  71. package/assets/rules/spec-driven.md +32 -0
  72. package/assets/rules/styling-bem.md +19 -0
  73. package/assets/rules/task-flow.md +148 -18
  74. package/assets/rules/testing.md +62 -0
  75. package/assets/rules/translations.md +21 -0
  76. package/assets/rules/typescript-conventions.md +28 -0
  77. package/assets/samples/specs/_template/spec.md +83 -0
  78. package/assets/samples/tasks/_template/grill.md +28 -0
  79. package/assets/samples/tasks/_template/plan.md +39 -0
  80. package/assets/samples/tasks/_template/progress.md +23 -0
  81. package/assets/skills/agent-kit.md +52 -2
  82. package/assets/templates/proposal.md +21 -0
  83. package/assets/templates/rule.md +31 -2
  84. package/bin/agent-kit.d.ts.map +1 -1
  85. package/bin/agent-kit.js +115 -87
  86. package/bin/agent-kit.js.map +1 -1
  87. package/index.d.ts +1 -0
  88. package/index.d.ts.map +1 -1
  89. package/index.js +1 -0
  90. package/index.js.map +1 -1
  91. package/lib/argv.d.ts.map +1 -1
  92. package/lib/argv.js +6 -4
  93. package/lib/argv.js.map +1 -1
  94. package/lib/assets.d.ts.map +1 -1
  95. package/lib/assets.js +2 -1
  96. package/lib/assets.js.map +1 -1
  97. package/lib/cargo.d.ts +20 -0
  98. package/lib/cargo.d.ts.map +1 -1
  99. package/lib/cargo.js.map +1 -1
  100. package/lib/cascade.d.ts +55 -0
  101. package/lib/cascade.d.ts.map +1 -0
  102. package/lib/cascade.js +131 -0
  103. package/lib/cascade.js.map +1 -0
  104. package/lib/catalog.d.ts +0 -75
  105. package/lib/catalog.d.ts.map +1 -1
  106. package/lib/catalog.js +44 -127
  107. package/lib/catalog.js.map +1 -1
  108. package/lib/commands.d.ts.map +1 -1
  109. package/lib/commands.js +153 -85
  110. package/lib/commands.js.map +1 -1
  111. package/lib/companion.d.ts.map +1 -1
  112. package/lib/companion.js +5 -5
  113. package/lib/companion.js.map +1 -1
  114. package/lib/config.d.ts +12 -1
  115. package/lib/config.d.ts.map +1 -1
  116. package/lib/config.js +11 -5
  117. package/lib/config.js.map +1 -1
  118. package/lib/enroll.d.ts +56 -0
  119. package/lib/enroll.d.ts.map +1 -0
  120. package/lib/enroll.js +123 -0
  121. package/lib/enroll.js.map +1 -0
  122. package/lib/freshness.d.ts.map +1 -1
  123. package/lib/freshness.js +31 -17
  124. package/lib/freshness.js.map +1 -1
  125. package/lib/hooks-map.d.ts +30 -0
  126. package/lib/hooks-map.d.ts.map +1 -1
  127. package/lib/hooks-map.js +80 -18
  128. package/lib/hooks-map.js.map +1 -1
  129. package/lib/integrity.d.ts +1 -2
  130. package/lib/integrity.d.ts.map +1 -1
  131. package/lib/integrity.js +0 -1
  132. package/lib/integrity.js.map +1 -1
  133. package/lib/observations.d.ts.map +1 -1
  134. package/lib/observations.js +25 -12
  135. package/lib/observations.js.map +1 -1
  136. package/lib/order.d.ts +10 -0
  137. package/lib/order.d.ts.map +1 -0
  138. package/lib/order.js +14 -0
  139. package/lib/order.js.map +1 -0
  140. package/lib/picker.d.ts.map +1 -1
  141. package/lib/picker.js +8 -2
  142. package/lib/picker.js.map +1 -1
  143. package/lib/plan.js +1 -1
  144. package/lib/plan.js.map +1 -1
  145. package/lib/proposals.d.ts.map +1 -1
  146. package/lib/proposals.js +25 -8
  147. package/lib/proposals.js.map +1 -1
  148. package/lib/sections.js +1 -1
  149. package/lib/sections.js.map +1 -1
  150. package/lib/ship.d.ts.map +1 -1
  151. package/lib/ship.js +9 -1
  152. package/lib/ship.js.map +1 -1
  153. package/lib/shipment.d.ts.map +1 -1
  154. package/lib/shipment.js +14 -10
  155. package/lib/shipment.js.map +1 -1
  156. package/lib/snapshot.d.ts.map +1 -1
  157. package/lib/snapshot.js +2 -1
  158. package/lib/snapshot.js.map +1 -1
  159. package/lib/stamp.js +1 -1
  160. package/lib/stamp.js.map +1 -1
  161. package/lib/sync.d.ts +12 -2
  162. package/lib/sync.d.ts.map +1 -1
  163. package/lib/sync.js +11 -10
  164. package/lib/sync.js.map +1 -1
  165. package/lib/vars.d.ts.map +1 -1
  166. package/lib/vars.js +2 -3
  167. package/lib/vars.js.map +1 -1
  168. package/package.json +1 -1
  169. package/rt-tools-agent-kit-0.9.0.tgz +0 -0
  170. package/rt-tools-agent-kit-0.8.2.tgz +0 -0
package/lib/hooks-map.js CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { existsSync, readFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
+ import { byKey } from './order.js';
19
20
  /** Настройка агента, в которой живёт карта. Путь от корня дерева. */
20
21
  export const SETTINGS_PATH = '.claude/settings.json';
21
22
  const DECLARATION = /^#\s*rt-hook:\s*(\S+)(?:[ \t]+(\S.*))?$/gm;
@@ -47,8 +48,8 @@ export function hooksSection(bindings) {
47
48
  events.set(binding.event, byMatcher);
48
49
  }
49
50
  const section = {};
50
- for (const [event, byMatcher] of [...events].sort()) {
51
- section[event] = [...byMatcher].sort().map(([matcher, paths]) => {
51
+ for (const [event, byMatcher] of [...events].sort(byKey)) {
52
+ section[event] = [...byMatcher].sort(byKey).map(([matcher, paths]) => {
52
53
  const hooks = paths.map((path) => ({
53
54
  type: 'command',
54
55
  command: `$CLAUDE_PROJECT_DIR/${path}`,
@@ -60,6 +61,37 @@ export function hooksSection(bindings) {
60
61
  }
61
62
  return section;
62
63
  }
64
+ /** Команды одной записи настройки: у всех у них общий образец — он объявлен записью, а не командой. */
65
+ function hooksOfRecord(event, record) {
66
+ const asked = record?.['matcher'];
67
+ const matcher = typeof asked === 'string' ? asked.trim() : '';
68
+ const commands = record?.['hooks'];
69
+ const found = [];
70
+ for (const command of Array.isArray(commands) ? commands : []) {
71
+ const line = command?.['command'];
72
+ if (typeof line === 'string') {
73
+ const at = line.indexOf('.claude/');
74
+ found.push({ event, matcher, path: at < 0 ? line : line.slice(at) });
75
+ }
76
+ }
77
+ return found;
78
+ }
79
+ /** Все команды гардов из настройки агента плоским списком. Текст не разбирается — отказ разбора. */
80
+ function hooksInSettings(text) {
81
+ const settings = JSON.parse(text);
82
+ const hooks = settings?.['hooks'];
83
+ const found = [];
84
+ for (const [event, records] of Object.entries((hooks ?? {}))) {
85
+ for (const record of Array.isArray(records) ? records : []) {
86
+ found.push(...hooksOfRecord(event, record));
87
+ }
88
+ }
89
+ return found;
90
+ }
91
+ /** Ключ, которым гард узнаётся: событие и путь вместе — на двух событиях он стоит дважды. */
92
+ function keyOf(hook) {
93
+ return `${hook.event} ${hook.path}`;
94
+ }
63
95
  /** Пути гардов, названные в настройке агента этого дерева. Нет файла — ни одного. */
64
96
  export function boundInSettings(root) {
65
97
  const path = join(root, SETTINGS_PATH);
@@ -76,27 +108,13 @@ export function boundInSettings(root) {
76
108
  // Сперва разбором: гард, стоящий на двух событиях, подключается к каждому отдельно, и по
77
109
  // одному имени файла этого не увидеть — подключённый к первому событию выглядел бы
78
110
  // подключённым и ко второму.
79
- const byEvent = [];
80
111
  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;
112
+ return hooksInSettings(text).map(keyOf);
95
113
  }
96
114
  catch {
97
115
  // Настройку дерево вправе держать так, как ему удобно, — вплоть до комментариев, которых
98
116
  // JSON не разбирает. Тогда ищется одно: назван ли путь гарда хоть где-нибудь в ней.
99
- return [...text.matchAll(/[\w./$-]*\.claude\/hooks\/[\w.-]+\.sh/g)].map((found) => found[0].replace(/^.*?(\.claude\/)/, '$1'));
117
+ return [...text.matchAll(/\.claude\/hooks\/[\w.-]+\.sh/g)].map((found) => found[0]);
100
118
  }
101
119
  }
102
120
  /**
@@ -110,4 +128,48 @@ export function unboundHooks(bindings, root) {
110
128
  const bound = new Set(boundInSettings(root));
111
129
  return bindings.filter((binding) => !bound.has(`${binding.event} ${binding.path}`) && !bound.has(binding.path));
112
130
  }
131
+ /**
132
+ * Образцы, под которыми гарды стоят в настройке агента: ключ — «событие и путь».
133
+ *
134
+ * Настройку, которую не разобрать, читать здесь нечем: образец лежит полем объекта, и вынуть его
135
+ * из текста поиском значило бы гадать. Тогда карта пуста, и расхождений не находится ни одного —
136
+ * это честнее, чем назвать расхождением то, чего не прочитали.
137
+ */
138
+ export function matchersInSettings(root) {
139
+ const path = join(root, SETTINGS_PATH);
140
+ if (!existsSync(path)) {
141
+ return new Map();
142
+ }
143
+ try {
144
+ return new Map(hooksInSettings(readFileSync(path, 'utf8')).map((one) => [keyOf(one), one.matcher]));
145
+ }
146
+ catch {
147
+ return new Map();
148
+ }
149
+ }
150
+ /**
151
+ * Расхождения объявления гарда с образцом, под которым его зовут.
152
+ *
153
+ * Гард, подписанный не на то, что объявляет, хуже неподключённого: снаружи он выглядит
154
+ * работающим — путь его в настройке назван, файл разложен, набор сценариев зелёный, — а вызов,
155
+ * ради которого его тело и написано, до него не доходит никогда. Так гейт правил и разбирал
156
+ * вызовы браузера веткой, которая не исполнялась ни разу: набор звал гард напрямую с
157
+ * подставленным вводом и объявления не читал вовсе.
158
+ *
159
+ * Судятся только подключённые гарды: о неподключённых говорит своя строка, и назвать их дважды
160
+ * значило бы спрятать настоящее расхождение среди повторов.
161
+ */
162
+ export function driftedMatchers(bindings, root) {
163
+ const bound = matchersInSettings(root);
164
+ const drifts = [];
165
+ for (const binding of bindings) {
166
+ const key = `${binding.event} ${binding.path}`;
167
+ const inSettings = bound.get(key);
168
+ if (inSettings === undefined || inSettings === binding.matcher) {
169
+ continue;
170
+ }
171
+ drifts.push({ event: binding.event, path: binding.path, declared: binding.matcher, bound: inSettings });
172
+ }
173
+ return drifts;
174
+ }
113
175
  //# 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,2CAA2C,CAAC;AAcxE;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAY;IACjD,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAuB,EAAgB,EAAE,CAAC,CAAC;QACnF,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QACf,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;QAChC,IAAI;KACP,CAAC,CAAC,CAAC;AACR,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,yFAAyF;IACzF,mFAAmF;IACnF,6BAA6B;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,CAAC;QACD,MAAM,QAAQ,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAa,QAA2C,EAAE,CAAC,OAAO,CAAC,CAAC;QAC/E,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC,EAAE,CAAC;YACtF,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACzD,MAAM,QAAQ,GAAa,MAAyC,EAAE,CAAC,OAAO,CAAC,CAAC;gBAChF,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;oBAC5D,MAAM,IAAI,GAAa,OAA0C,EAAE,CAAC,SAAS,CAAC,CAAC;oBAC/E,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;wBAC3B,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;oBACvE,CAAC;gBACL,CAAC;YACL,CAAC;QACL,CAAC;QAED,OAAO,OAAO,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACL,yFAAyF;QACzF,oFAAoF;QACpF,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;IACN,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,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,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3I,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.*))?$/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"]}
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';\nimport { byKey } from './order.js';\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(byKey)) {\n section[event] = [...byMatcher].sort(byKey).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/** Одна команда гарда в настройке агента: событие, путь и образец, под которым она стоит. */\ninterface ISettingsHook {\n readonly event: string;\n readonly path: string;\n readonly matcher: string;\n}\n\n/** Команды одной записи настройки: у всех у них общий образец — он объявлен записью, а не командой. */\nfunction hooksOfRecord(event: string, record: unknown): readonly ISettingsHook[] {\n const asked: unknown = (record as Record<string, unknown> | null)?.['matcher'];\n const matcher: string = typeof asked === 'string' ? asked.trim() : '';\n const commands: unknown = (record as Record<string, unknown> | null)?.['hooks'];\n const found: ISettingsHook[] = [];\n\n for (const command of Array.isArray(commands) ? commands : []) {\n const line: unknown = (command as Record<string, unknown> | null)?.['command'];\n\n if (typeof line === 'string') {\n const at: number = line.indexOf('.claude/');\n\n found.push({ event, matcher, path: at < 0 ? line : line.slice(at) });\n }\n }\n\n return found;\n}\n\n/** Все команды гардов из настройки агента плоским списком. Текст не разбирается — отказ разбора. */\nfunction hooksInSettings(text: string): readonly ISettingsHook[] {\n const settings: unknown = JSON.parse(text);\n const hooks: unknown = (settings as Record<string, unknown> | null)?.['hooks'];\n const found: ISettingsHook[] = [];\n\n for (const [event, records] of Object.entries((hooks ?? {}) as Record<string, unknown>)) {\n for (const record of Array.isArray(records) ? records : []) {\n found.push(...hooksOfRecord(event, record));\n }\n }\n\n return found;\n}\n\n/** Ключ, которым гард узнаётся: событие и путь вместе — на двух событиях он стоит дважды. */\nfunction keyOf(hook: ISettingsHook): string {\n return `${hook.event} ${hook.path}`;\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 try {\n return hooksInSettings(text).map(keyOf);\n } catch {\n // Настройку дерево вправе держать так, как ему удобно, — вплоть до комментариев, которых\n // JSON не разбирает. Тогда ищется одно: назван ли путь гарда хоть где-нибудь в ней.\n return [...text.matchAll(/\\.claude\\/hooks\\/[\\w.-]+\\.sh/g)].map((found: RegExpMatchArray): string => found[0]);\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\n/**\n * Образцы, под которыми гарды стоят в настройке агента: ключ — «событие и путь».\n *\n * Настройку, которую не разобрать, читать здесь нечем: образец лежит полем объекта, и вынуть его\n * из текста поиском значило бы гадать. Тогда карта пуста, и расхождений не находится ни одного —\n * это честнее, чем назвать расхождением то, чего не прочитали.\n */\nexport function matchersInSettings(root: string): ReadonlyMap<string, string> {\n const path: string = join(root, SETTINGS_PATH);\n if (!existsSync(path)) {\n return new Map();\n }\n\n try {\n return new Map(\n hooksInSettings(readFileSync(path, 'utf8')).map((one: ISettingsHook): [string, string] => [keyOf(one), one.matcher])\n );\n } catch {\n return new Map();\n }\n}\n\n/** Гард, чьё объявление разошлось с образцом, под которым его зовут. */\nexport interface IMatcherDrift {\n readonly event: string;\n readonly path: string;\n /** Что гард объявляет о себе строкой `# rt-hook:`. */\n readonly declared: string;\n /** Под чем он на самом деле стоит в настройке агента. */\n readonly bound: string;\n}\n\n/**\n * Расхождения объявления гарда с образцом, под которым его зовут.\n *\n * Гард, подписанный не на то, что объявляет, хуже неподключённого: снаружи он выглядит\n * работающим — путь его в настройке назван, файл разложен, набор сценариев зелёный, — а вызов,\n * ради которого его тело и написано, до него не доходит никогда. Так гейт правил и разбирал\n * вызовы браузера веткой, которая не исполнялась ни разу: набор звал гард напрямую с\n * подставленным вводом и объявления не читал вовсе.\n *\n * Судятся только подключённые гарды: о неподключённых говорит своя строка, и назвать их дважды\n * значило бы спрятать настоящее расхождение среди повторов.\n */\nexport function driftedMatchers(bindings: readonly IHookBinding[], root: string): readonly IMatcherDrift[] {\n const bound: ReadonlyMap<string, string> = matchersInSettings(root);\n const drifts: IMatcherDrift[] = [];\n\n for (const binding of bindings) {\n const key: string = `${binding.event} ${binding.path}`;\n const inSettings: string | undefined = bound.get(key);\n if (inSettings === undefined || inSettings === binding.matcher) {\n continue;\n }\n drifts.push({ event: binding.event, path: binding.path, declared: binding.matcher, bound: inSettings });\n }\n\n return drifts;\n}\n"]}
@@ -8,9 +8,8 @@
8
8
  * Разбирается только вступление между `---`: остальное в файле — проза, и `law:` посреди неё
9
9
  * означает пример, а не объявление.
10
10
  */
11
- import { frontMatterOf, IEntryOfCatalog, IFrontMatter } from './catalog.js';
11
+ import { IEntryOfCatalog } from './catalog.js';
12
12
  import { TKind } from './config.js';
13
- export { frontMatterOf, IFrontMatter };
14
13
  /** Расхождение в ресурсах пакета: кто ссылается, на что и чего не нашлось. */
15
14
  export interface IBrokenLink {
16
15
  readonly id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"integrity.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/integrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,WAAW,EAAE,CAoBvF;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,cAAc,EAAE,CAc7F"}
1
+ {"version":3,"file":"integrity.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/integrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAiB,eAAe,EAAgB,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,WAAW,EAAE,CAoBvF;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,cAAc,EAAE,CAc7F"}
package/lib/integrity.js CHANGED
@@ -9,7 +9,6 @@
9
9
  * означает пример, а не объявление.
10
10
  */
11
11
  import { frontMatterOf } from './catalog.js';
12
- export { frontMatterOf };
13
12
  /**
14
13
  * Ссылки шапок, которым в каталоге ничего не отвечает.
15
14
  *
@@ -1 +1 @@
1
- {"version":3,"file":"integrity.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/integrity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Целостность самих ресурсов пакета: у правила есть закон, у паттерна — правило.\n *\n * Проверка нужна не проекту, а пакету. Закон переименовали, а `law:` у правила осталось\n * прежним — раскладка от этого не падает: она кладёт файлы, а не читает их шапки. Промах уезжает\n * к проектам, и находит его тот, кто пошёл за законом по ссылке из правила и не нашёл его.\n *\n * Разбирается только вступление между `---`: остальное в файле — проза, и `law:` посреди неё\n * означает пример, а не объявление.\n */\nimport { frontMatterOf, IEntryOfCatalog, IFrontMatter } from './catalog.js';\nimport { TKind } from './config.js';\n\nexport { frontMatterOf, IFrontMatter };\n\n/** Расхождение в ресурсах пакета: кто ссылается, на что и чего не нашлось. */\nexport interface IBrokenLink {\n readonly id: string;\n readonly field: 'law' | 'rule';\n readonly wanted: string;\n}\n\n/**\n * Ссылки шапок, которым в каталоге ничего не отвечает.\n *\n * Закон ищется по короткому имени: слой в ссылке не отражается, и переезд закона между слоями\n * шапок правил не переписывает — так же, как этого не делают ни таблицы законов, ни спеки.\n */\nexport function brokenLinks(catalog: readonly IEntryOfCatalog[]): readonly IBrokenLink[] {\n const shortOf: (entry: IEntryOfCatalog) => string = (entry: IEntryOfCatalog): string => entry.name.split('/').pop() ?? entry.name;\n const laws: ReadonlySet<string> = new Set(catalog.filter((entry: IEntryOfCatalog): boolean => entry.kind === 'laws').map(shortOf));\n const rules: ReadonlySet<string> = new Set(catalog.filter((entry: IEntryOfCatalog): boolean => entry.kind === 'rules').map(shortOf));\n\n const broken: IBrokenLink[] = [];\n for (const entry of catalog) {\n if (entry.kind !== 'rules' && entry.kind !== 'patterns') {\n continue;\n }\n const head: IFrontMatter = frontMatterOf(entry.text);\n if (entry.kind === 'rules' && !laws.has(head.law)) {\n broken.push({ id: entry.id, field: 'law', wanted: head.law });\n }\n if (entry.kind === 'patterns' && !rules.has(head.rule)) {\n broken.push({ id: entry.id, field: 'rule', wanted: head.rule });\n }\n }\n\n return broken;\n}\n\n/** Род и короткое имя, под которым в наборе лежит больше одного ресурса. */\nexport interface IAmbiguousName {\n readonly kind: TKind;\n /** Последнее звено имени — то, чем ресурсы ссылаются друг на друга. */\n readonly name: string;\n readonly ids: readonly string[];\n}\n\n/** Ресурсы, сошедшиеся на одном коротком имени, вместе с полными именами, которыми они зовутся. */\ninterface INamedGroup {\n readonly kind: TKind;\n readonly name: string;\n readonly ids: string[];\n readonly names: Set<string>;\n}\n\n/**\n * Ресурсы одного рода с одинаковым последним звеном имени.\n *\n * Связь потомка с родителем ищется по этому звену — и полного пути в шапке нет намеренно, чтобы\n * переезд закона между слоями не переписывал шапки всех правил при нём. Два закона с именем\n * `access` в разных слоях делают такую ссылку двусмысленной: каскад снял бы потомков не того\n * родителя, и молча — оба имени существуют, и промахом ни одно из них не выглядит.\n *\n * Судится здесь, до всякой раскладки в дереве: набор с двусмысленным именем неисправен сам, у\n * любого потребителя разом.\n *\n * Виды одного ресурса двусмысленности не дают: у них совпадает не только последнее звено, но и\n * имя целиком, и ссылка при них указывает на один ресурс — тот, чей вид выбрало дерево.\n */\nexport function ambiguousNames(catalog: readonly IEntryOfCatalog[]): readonly IAmbiguousName[] {\n const found: Map<string, INamedGroup> = new Map<string, INamedGroup>();\n for (const entry of catalog) {\n const short: string = entry.name.split('/').pop() ?? entry.name;\n const key: string = `${entry.kind} ${short}`;\n const group: INamedGroup = found.get(key) ?? { kind: entry.kind, name: short, ids: [], names: new Set<string>() };\n group.ids.push(entry.id);\n group.names.add(entry.name);\n found.set(key, group);\n }\n\n return [...found.values()]\n .filter((group: INamedGroup): boolean => group.names.size > 1)\n .map((group: INamedGroup): IAmbiguousName => ({ kind: group.kind, name: group.name, ids: group.ids }));\n}\n"]}
1
+ {"version":3,"file":"integrity.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/integrity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Целостность самих ресурсов пакета: у правила есть закон, у паттерна — правило.\n *\n * Проверка нужна не проекту, а пакету. Закон переименовали, а `law:` у правила осталось\n * прежним — раскладка от этого не падает: она кладёт файлы, а не читает их шапки. Промах уезжает\n * к проектам, и находит его тот, кто пошёл за законом по ссылке из правила и не нашёл его.\n *\n * Разбирается только вступление между `---`: остальное в файле — проза, и `law:` посреди неё\n * означает пример, а не объявление.\n */\nimport { frontMatterOf, IEntryOfCatalog, IFrontMatter } from './catalog.js';\nimport { TKind } from './config.js';\n\n/** Расхождение в ресурсах пакета: кто ссылается, на что и чего не нашлось. */\nexport interface IBrokenLink {\n readonly id: string;\n readonly field: 'law' | 'rule';\n readonly wanted: string;\n}\n\n/**\n * Ссылки шапок, которым в каталоге ничего не отвечает.\n *\n * Закон ищется по короткому имени: слой в ссылке не отражается, и переезд закона между слоями\n * шапок правил не переписывает — так же, как этого не делают ни таблицы законов, ни спеки.\n */\nexport function brokenLinks(catalog: readonly IEntryOfCatalog[]): readonly IBrokenLink[] {\n const shortOf: (entry: IEntryOfCatalog) => string = (entry: IEntryOfCatalog): string => entry.name.split('/').pop() ?? entry.name;\n const laws: ReadonlySet<string> = new Set(catalog.filter((entry: IEntryOfCatalog): boolean => entry.kind === 'laws').map(shortOf));\n const rules: ReadonlySet<string> = new Set(catalog.filter((entry: IEntryOfCatalog): boolean => entry.kind === 'rules').map(shortOf));\n\n const broken: IBrokenLink[] = [];\n for (const entry of catalog) {\n if (entry.kind !== 'rules' && entry.kind !== 'patterns') {\n continue;\n }\n const head: IFrontMatter = frontMatterOf(entry.text);\n if (entry.kind === 'rules' && !laws.has(head.law)) {\n broken.push({ id: entry.id, field: 'law', wanted: head.law });\n }\n if (entry.kind === 'patterns' && !rules.has(head.rule)) {\n broken.push({ id: entry.id, field: 'rule', wanted: head.rule });\n }\n }\n\n return broken;\n}\n\n/** Род и короткое имя, под которым в наборе лежит больше одного ресурса. */\nexport interface IAmbiguousName {\n readonly kind: TKind;\n /** Последнее звено имени — то, чем ресурсы ссылаются друг на друга. */\n readonly name: string;\n readonly ids: readonly string[];\n}\n\n/** Ресурсы, сошедшиеся на одном коротком имени, вместе с полными именами, которыми они зовутся. */\ninterface INamedGroup {\n readonly kind: TKind;\n readonly name: string;\n readonly ids: string[];\n readonly names: Set<string>;\n}\n\n/**\n * Ресурсы одного рода с одинаковым последним звеном имени.\n *\n * Связь потомка с родителем ищется по этому звену — и полного пути в шапке нет намеренно, чтобы\n * переезд закона между слоями не переписывал шапки всех правил при нём. Два закона с именем\n * `access` в разных слоях делают такую ссылку двусмысленной: каскад снял бы потомков не того\n * родителя, и молча — оба имени существуют, и промахом ни одно из них не выглядит.\n *\n * Судится здесь, до всякой раскладки в дереве: набор с двусмысленным именем неисправен сам, у\n * любого потребителя разом.\n *\n * Виды одного ресурса двусмысленности не дают: у них совпадает не только последнее звено, но и\n * имя целиком, и ссылка при них указывает на один ресурс — тот, чей вид выбрало дерево.\n */\nexport function ambiguousNames(catalog: readonly IEntryOfCatalog[]): readonly IAmbiguousName[] {\n const found: Map<string, INamedGroup> = new Map<string, INamedGroup>();\n for (const entry of catalog) {\n const short: string = entry.name.split('/').pop() ?? entry.name;\n const key: string = `${entry.kind} ${short}`;\n const group: INamedGroup = found.get(key) ?? { kind: entry.kind, name: short, ids: [], names: new Set<string>() };\n group.ids.push(entry.id);\n group.names.add(entry.name);\n found.set(key, group);\n }\n\n return [...found.values()]\n .filter((group: INamedGroup): boolean => group.names.size > 1)\n .map((group: INamedGroup): IAmbiguousName => ({ kind: group.kind, name: group.name, ids: group.ids }));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"observations.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/observations.ts"],"names":[],"mappings":"AAeA,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,EAAE,MAAsC,CAAC;AAEtE,oGAAoG;AACpG,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC;AAEpC,gEAAgE;AAChE,eAAO,MAAM,YAAY,EAAE,MAAU,CAAC;AAEtC,yCAAyC;AACzC,MAAM,MAAM,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,CAAC;AAE/D,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B;AAED,qFAAqF;AACrF,MAAM,WAAW,MAAM;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAOD,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAoBlE;AAED,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/C,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAiCvF;AAoBD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAgBjH"}
1
+ {"version":3,"file":"observations.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/observations.ts"],"names":[],"mappings":"AAgBA,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,EAAE,MAAsC,CAAC;AAEtE,oGAAoG;AACpG,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC;AAEpC,gEAAgE;AAChE,eAAO,MAAM,YAAY,EAAE,MAAU,CAAC;AAEtC,yCAAyC;AACzC,MAAM,MAAM,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,CAAC;AAE/D,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B;AAED,qFAAqF;AACrF,MAAM,WAAW,MAAM;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAOD,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAoBlE;AAED,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/C,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC5B;AAsBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CA8BvF;AAoBD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAgBjH"}
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
+ import { byText } from './order.js';
15
16
  /** Куда `observe.sh` пишет наблюдения. Путь от корня дерева. */
16
17
  export const OBSERVATIONS_DIR = '.claude/rt-kit/observations';
17
18
  /** Сколько дней наблюдения лежат. Каталог, который только растёт, через год перестают открывать. */
@@ -51,6 +52,21 @@ export function parseObservation(line) {
51
52
  return null;
52
53
  }
53
54
  }
55
+ /** Раньше ли день черты. Дни записаны `ГГГГ-ММ-ДД`, и порядок строк у них хронологический. */
56
+ function isBefore(day, edge) {
57
+ return byText(day, edge) < 0;
58
+ }
59
+ /** Записи одного дня из его файла. Строка, которая не разбирается, пропускается молча. */
60
+ function observationsOfDay(path) {
61
+ const found = [];
62
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
63
+ const one = parseObservation(line);
64
+ if (one) {
65
+ found.push(one);
66
+ }
67
+ }
68
+ return found;
69
+ }
54
70
  /**
55
71
  * Наблюдения за отрезок и уборка того, что старше срока хранения.
56
72
  *
@@ -66,24 +82,21 @@ export function readObservations(root, today, days) {
66
82
  const keepFrom = dayBefore(today, KEEP_DAYS);
67
83
  const observations = [];
68
84
  const swept = [];
69
- for (const name of readdirSync(dir).sort()) {
85
+ for (const name of readdirSync(dir).sort(byText)) {
70
86
  const day = dayOf(name);
71
87
  if (!day) {
72
88
  continue;
73
89
  }
74
- if (day < keepFrom) {
90
+ if (isBefore(day, keepFrom)) {
75
91
  rmSync(join(dir, name), { force: true });
76
92
  swept.push(name);
77
- continue;
78
93
  }
79
- if (day < since) {
80
- continue;
94
+ else if (isBefore(day, since)) {
95
+ // День старше запрошенного отрезка, но моложе черты хранения: файл остаётся лежать,
96
+ // а в этот отчёт не идёт
81
97
  }
82
- for (const line of readFileSync(join(dir, name), 'utf8').split('\n')) {
83
- const found = parseObservation(line);
84
- if (found) {
85
- observations.push(found);
86
- }
98
+ else {
99
+ observations.push(...observationsOfDay(join(dir, name)));
87
100
  }
88
101
  }
89
102
  return { observations, swept, silent: false };
@@ -118,8 +131,8 @@ export function summarize(observations, known, days) {
118
131
  denials: countBy(denials.map((entry) => entry.resource)),
119
132
  kinds: countBy(denials.map((entry) => entry.kind)),
120
133
  guards: countBy(of(observations, 'guard-deny').map((entry) => entry.resource)),
121
- unused: known.filter((name) => !loaded.has(name)).sort(),
122
- versions: [...new Set(observations.map((entry) => entry.version).filter(Boolean))].sort(),
134
+ unused: known.filter((name) => !loaded.has(name)).sort(byText),
135
+ versions: [...new Set(observations.map((entry) => entry.version).filter(Boolean))].sort(byText),
123
136
  total: observations.length,
124
137
  };
125
138
  }
@@ -1 +1 @@
1
- {"version":3,"file":"observations.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/observations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,gEAAgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAW,6BAA6B,CAAC;AAEtE,oGAAoG;AACpG,MAAM,CAAC,MAAM,SAAS,GAAW,EAAE,CAAC;AAEpC,gEAAgE;AAChE,MAAM,CAAC,MAAM,YAAY,GAAW,CAAC,CAAC;AA0CtC,qFAAqF;AACrF,MAAM,QAAQ,GAAW,8BAA8B,CAAC;AAExD,MAAM,KAAK,GAAoC,CAAC,IAAY,EAAiB,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAEjH,gGAAgG;AAChG,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,IAAY;IACjD,MAAM,EAAE,GAAW,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC;IAEpD,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IACzC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,CAAC;QACD,MAAM,KAAK,GAA4B,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QACnF,MAAM,KAAK,GAAY,KAAK,CAAC,IAAI,CAAC,CAAC;QAEnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK;YACrC,CAAC,CAAC;gBACI,KAAK;gBACL,QAAQ,EAAE,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE;gBAC9D,IAAI,EAAE,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE;gBAC5D,OAAO,EAAE,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE;gBAC7D,OAAO,EAAE,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;aAC5D;YACH,CAAC,CAAC,IAAI,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC;AAUD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa,EAAE,IAAY;IACtE,MAAM,GAAG,GAAW,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACjD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC;IAED,MAAM,KAAK,GAAW,SAAS,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAW,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,YAAY,GAAmB,EAAE,CAAC;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAkB,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG,EAAE,CAAC;YACP,SAAS;QACb,CAAC;QACD,IAAI,GAAG,GAAG,QAAQ,EAAE,CAAC;YACjB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,SAAS;QACb,CAAC;QACD,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;YACd,SAAS;QACb,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACnE,MAAM,KAAK,GAAwB,gBAAgB,CAAC,IAAI,CAAC,CAAC;YAC1D,IAAI,KAAK,EAAE,CAAC;gBACR,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC7B,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,OAAO,GAAqD,CAAC,MAAyB,EAAqB,EAAE;IAC/G,MAAM,OAAO,GAAwB,IAAI,GAAG,EAAE,CAAC;IAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,KAAK,EAAE,CAAC;YACR,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACtD,CAAC;IACL,CAAC;IAED,OAAO,CAAC,GAAG,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAmB,EAAU,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;SACnE,IAAI,CAAC,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;AACxH,CAAC,CAAC;AAEF,MAAM,EAAE,GAAsF,CAC1F,YAAqC,EACrC,KAAa,EACU,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,KAAmB,EAAW,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;AAE3G;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,YAAqC,EAAE,KAAwB,EAAE,IAAY;IACnG,MAAM,KAAK,GAA4B,EAAE,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IACtE,MAAM,OAAO,GAA4B,EAAE,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvE,MAAM,MAAM,GAAgB,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEhG,OAAO;QACH,IAAI;QACJ,QAAQ,EAAE,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;QACxG,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1E,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC9E,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACxE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACpG,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE;QACzE,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;QAC/G,KAAK,EAAE,YAAY,CAAC,MAAM;KAC7B,CAAC;AACN,CAAC","sourcesContent":["/**\n * Наблюдения и сводка по ним.\n *\n * Наблюдения пишет `hooks/observe.sh` — по строке на событие, по файлу на день. Здесь они\n * читаются и сводятся: чем пользовались, чем не пользовались ни разу, обо что спотыкались.\n *\n * Разбор отделён от чтения диска намеренно: сводка — то, ради чего наблюдения ведутся, и\n * проверять её на подложенных файлах значило бы проверять файловую систему.\n *\n * Дата приходит доводом, а не берётся из часов: сводка за отрезок дней иначе не проверяется\n * спекой — вчерашний файл фикстуры завтра оказывается позавчерашним.\n */\nimport { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Куда `observe.sh` пишет наблюдения. Путь от корня дерева. */\nexport const OBSERVATIONS_DIR: string = '.claude/rt-kit/observations';\n\n/** Сколько дней наблюдения лежат. Каталог, который только растёт, через год перестают открывать. */\nexport const KEEP_DAYS: number = 30;\n\n/** За сколько дней сводка отвечает, когда отрезок не назван. */\nexport const DEFAULT_DAYS: number = 3;\n\n/** Роды событий, которые пишут гарды. */\nexport type TEvent = 'skill-load' | 'gate-deny' | 'guard-deny';\n\nexport interface IObservation {\n readonly event: string;\n /** Имя правила или гарда — имя ресурса пакета, и только оно. */\n readonly resource: string;\n /** Род правки: расширение, `command` или `browser`. Пути в наблюдении нет. */\n readonly kind: string;\n /** Признак сессии: считает заходы, имени сессии не выдаёт. */\n readonly session: string;\n readonly version: string;\n}\n\n/** Сколько раз что-то встретилось. Порядок — по убыванию, при равенстве по имени. */\nexport interface ICount {\n readonly name: string;\n readonly count: number;\n}\n\nexport interface ISummary {\n readonly days: number;\n /** Сколько заходов оставило хоть одно наблюдение. */\n readonly sessions: number;\n readonly loads: readonly ICount[];\n /** Отбития гейта по правилам. */\n readonly denials: readonly ICount[];\n /** Род правки, на котором гейт отбивал. */\n readonly kinds: readonly ICount[];\n readonly guards: readonly ICount[];\n /**\n * Правила, разложенные в дерево и не загруженные за отрезок ни разу. Мёртвый ресурс иначе\n * неотличим от работающего — и это единственная строка сводки, которую нельзя получить из\n * самих наблюдений.\n */\n readonly unused: readonly string[];\n readonly versions: readonly string[];\n readonly total: number;\n}\n\n/** Дата файла наблюдений: `2026-08-12.jsonl`. Чужое имя в каталоге не наблюдение. */\nconst DAY_FILE: RegExp = /^(\\d{4}-\\d{2}-\\d{2})\\.jsonl$/;\n\nconst dayOf: (name: string) => string | null = (name: string): string | null => DAY_FILE.exec(name)?.[1] ?? null;\n\n/** День, отстоящий от названного на столько-то суток назад. Считается в UTC — как и пишется. */\nexport function dayBefore(today: string, days: number): string {\n const at: number = Date.parse(`${today}T00:00:00Z`);\n\n return Number.isNaN(at) ? today : new Date(at - days * 86_400_000).toISOString().slice(0, 10);\n}\n\n/**\n * Одна строка наблюдения. Битая пропускается молча: наблюдение — побочная запись гарда, и\n * оборванная на полуслове строка не имеет права уронить сводку.\n */\nexport function parseObservation(line: string): IObservation | null {\n if (!line.trim()) {\n return null;\n }\n try {\n const found: Record<string, unknown> = JSON.parse(line) as Record<string, unknown>;\n const event: unknown = found['ev'];\n\n return typeof event === 'string' && event\n ? {\n event,\n resource: typeof found['res'] === 'string' ? found['res'] : '',\n kind: typeof found['kind'] === 'string' ? found['kind'] : '',\n session: typeof found['sid'] === 'string' ? found['sid'] : '',\n version: typeof found['v'] === 'string' ? found['v'] : '',\n }\n : null;\n } catch {\n return null;\n }\n}\n\nexport interface IReadResult {\n readonly observations: readonly IObservation[];\n /** Файлы, снятые по сроку хранения: их называют вслух, а не убирают молча. */\n readonly swept: readonly string[];\n /** Каталога нет вовсе — записи не велось ни разу. */\n readonly silent: boolean;\n}\n\n/**\n * Наблюдения за отрезок и уборка того, что старше срока хранения.\n *\n * Уборка идёт здесь, а не отдельной командой: каталог растёт от работы, а не от запусков, и\n * команда, о которой надо помнить, до второго месяца не доживает.\n */\nexport function readObservations(root: string, today: string, days: number): IReadResult {\n const dir: string = join(root, OBSERVATIONS_DIR);\n if (!existsSync(dir)) {\n return { observations: [], swept: [], silent: true };\n }\n\n const since: string = dayBefore(today, days - 1);\n const keepFrom: string = dayBefore(today, KEEP_DAYS);\n const observations: IObservation[] = [];\n const swept: string[] = [];\n\n for (const name of readdirSync(dir).sort()) {\n const day: string | null = dayOf(name);\n if (!day) {\n continue;\n }\n if (day < keepFrom) {\n rmSync(join(dir, name), { force: true });\n swept.push(name);\n continue;\n }\n if (day < since) {\n continue;\n }\n for (const line of readFileSync(join(dir, name), 'utf8').split('\\n')) {\n const found: IObservation | null = parseObservation(line);\n if (found) {\n observations.push(found);\n }\n }\n }\n\n return { observations, swept, silent: false };\n}\n\nconst countBy: (values: readonly string[]) => readonly ICount[] = (values: readonly string[]): readonly ICount[] => {\n const counted: Map<string, number> = new Map();\n for (const value of values) {\n if (value) {\n counted.set(value, (counted.get(value) ?? 0) + 1);\n }\n }\n\n return [...counted]\n .map(([name, count]: [string, number]): ICount => ({ name, count }))\n .sort((left: ICount, right: ICount): number => right.count - left.count || left.name.localeCompare(right.name));\n};\n\nconst of: (observations: readonly IObservation[], event: TEvent) => readonly IObservation[] = (\n observations: readonly IObservation[],\n event: TEvent\n): readonly IObservation[] => observations.filter((entry: IObservation): boolean => entry.event === event);\n\n/**\n * Сводка за отрезок.\n *\n * `known` — имена правил, разложенных в это дерево. Без них сводка отвечает только на вопрос\n * «чем пользовались», а самое ценное — чем не пользовались ни разу — сказать нечем: в самих\n * наблюдениях незагруженного правила нет по определению.\n */\nexport function summarize(observations: readonly IObservation[], known: readonly string[], days: number): ISummary {\n const loads: readonly IObservation[] = of(observations, 'skill-load');\n const denials: readonly IObservation[] = of(observations, 'gate-deny');\n const loaded: Set<string> = new Set(loads.map((entry: IObservation): string => entry.resource));\n\n return {\n days,\n sessions: new Set(observations.map((entry: IObservation): string => entry.session).filter(Boolean)).size,\n loads: countBy(loads.map((entry: IObservation): string => entry.resource)),\n denials: countBy(denials.map((entry: IObservation): string => entry.resource)),\n kinds: countBy(denials.map((entry: IObservation): string => entry.kind)),\n guards: countBy(of(observations, 'guard-deny').map((entry: IObservation): string => entry.resource)),\n unused: known.filter((name: string): boolean => !loaded.has(name)).sort(),\n versions: [...new Set(observations.map((entry: IObservation): string => entry.version).filter(Boolean))].sort(),\n total: observations.length,\n };\n}\n"]}
1
+ {"version":3,"file":"observations.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/observations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Наблюдения и сводка по ним.\n *\n * Наблюдения пишет `hooks/observe.sh` — по строке на событие, по файлу на день. Здесь они\n * читаются и сводятся: чем пользовались, чем не пользовались ни разу, обо что спотыкались.\n *\n * Разбор отделён от чтения диска намеренно: сводка — то, ради чего наблюдения ведутся, и\n * проверять её на подложенных файлах значило бы проверять файловую систему.\n *\n * Дата приходит доводом, а не берётся из часов: сводка за отрезок дней иначе не проверяется\n * спекой — вчерашний файл фикстуры завтра оказывается позавчерашним.\n */\nimport { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { byText } from './order.js';\n\n/** Куда `observe.sh` пишет наблюдения. Путь от корня дерева. */\nexport const OBSERVATIONS_DIR: string = '.claude/rt-kit/observations';\n\n/** Сколько дней наблюдения лежат. Каталог, который только растёт, через год перестают открывать. */\nexport const KEEP_DAYS: number = 30;\n\n/** За сколько дней сводка отвечает, когда отрезок не назван. */\nexport const DEFAULT_DAYS: number = 3;\n\n/** Роды событий, которые пишут гарды. */\nexport type TEvent = 'skill-load' | 'gate-deny' | 'guard-deny';\n\nexport interface IObservation {\n readonly event: string;\n /** Имя правила или гарда — имя ресурса пакета, и только оно. */\n readonly resource: string;\n /** Род правки: расширение, `command` или `browser`. Пути в наблюдении нет. */\n readonly kind: string;\n /** Признак сессии: считает заходы, имени сессии не выдаёт. */\n readonly session: string;\n readonly version: string;\n}\n\n/** Сколько раз что-то встретилось. Порядок — по убыванию, при равенстве по имени. */\nexport interface ICount {\n readonly name: string;\n readonly count: number;\n}\n\nexport interface ISummary {\n readonly days: number;\n /** Сколько заходов оставило хоть одно наблюдение. */\n readonly sessions: number;\n readonly loads: readonly ICount[];\n /** Отбития гейта по правилам. */\n readonly denials: readonly ICount[];\n /** Род правки, на котором гейт отбивал. */\n readonly kinds: readonly ICount[];\n readonly guards: readonly ICount[];\n /**\n * Правила, разложенные в дерево и не загруженные за отрезок ни разу. Мёртвый ресурс иначе\n * неотличим от работающего — и это единственная строка сводки, которую нельзя получить из\n * самих наблюдений.\n */\n readonly unused: readonly string[];\n readonly versions: readonly string[];\n readonly total: number;\n}\n\n/** Дата файла наблюдений: `2026-08-12.jsonl`. Чужое имя в каталоге не наблюдение. */\nconst DAY_FILE: RegExp = /^(\\d{4}-\\d{2}-\\d{2})\\.jsonl$/;\n\nconst dayOf: (name: string) => string | null = (name: string): string | null => DAY_FILE.exec(name)?.[1] ?? null;\n\n/** День, отстоящий от названного на столько-то суток назад. Считается в UTC — как и пишется. */\nexport function dayBefore(today: string, days: number): string {\n const at: number = Date.parse(`${today}T00:00:00Z`);\n\n return Number.isNaN(at) ? today : new Date(at - days * 86_400_000).toISOString().slice(0, 10);\n}\n\n/**\n * Одна строка наблюдения. Битая пропускается молча: наблюдение — побочная запись гарда, и\n * оборванная на полуслове строка не имеет права уронить сводку.\n */\nexport function parseObservation(line: string): IObservation | null {\n if (!line.trim()) {\n return null;\n }\n try {\n const found: Record<string, unknown> = JSON.parse(line) as Record<string, unknown>;\n const event: unknown = found['ev'];\n\n return typeof event === 'string' && event\n ? {\n event,\n resource: typeof found['res'] === 'string' ? found['res'] : '',\n kind: typeof found['kind'] === 'string' ? found['kind'] : '',\n session: typeof found['sid'] === 'string' ? found['sid'] : '',\n version: typeof found['v'] === 'string' ? found['v'] : '',\n }\n : null;\n } catch {\n return null;\n }\n}\n\nexport interface IReadResult {\n readonly observations: readonly IObservation[];\n /** Файлы, снятые по сроку хранения: их называют вслух, а не убирают молча. */\n readonly swept: readonly string[];\n /** Каталога нет вовсе — записи не велось ни разу. */\n readonly silent: boolean;\n}\n\n/** Раньше ли день черты. Дни записаны `ГГГГ-ММ-ДД`, и порядок строк у них хронологический. */\nfunction isBefore(day: string, edge: string): boolean {\n return byText(day, edge) < 0;\n}\n\n/** Записи одного дня из его файла. Строка, которая не разбирается, пропускается молча. */\nfunction observationsOfDay(path: string): readonly IObservation[] {\n const found: IObservation[] = [];\n\n for (const line of readFileSync(path, 'utf8').split('\\n')) {\n const one: IObservation | null = parseObservation(line);\n\n if (one) {\n found.push(one);\n }\n }\n\n return found;\n}\n\n/**\n * Наблюдения за отрезок и уборка того, что старше срока хранения.\n *\n * Уборка идёт здесь, а не отдельной командой: каталог растёт от работы, а не от запусков, и\n * команда, о которой надо помнить, до второго месяца не доживает.\n */\nexport function readObservations(root: string, today: string, days: number): IReadResult {\n const dir: string = join(root, OBSERVATIONS_DIR);\n if (!existsSync(dir)) {\n return { observations: [], swept: [], silent: true };\n }\n\n const since: string = dayBefore(today, days - 1);\n const keepFrom: string = dayBefore(today, KEEP_DAYS);\n const observations: IObservation[] = [];\n const swept: string[] = [];\n\n for (const name of readdirSync(dir).sort(byText)) {\n const day: string | null = dayOf(name);\n\n if (!day) {\n continue;\n }\n\n if (isBefore(day, keepFrom)) {\n rmSync(join(dir, name), { force: true });\n swept.push(name);\n } else if (isBefore(day, since)) {\n // День старше запрошенного отрезка, но моложе черты хранения: файл остаётся лежать,\n // а в этот отчёт не идёт\n } else {\n observations.push(...observationsOfDay(join(dir, name)));\n }\n }\n\n return { observations, swept, silent: false };\n}\n\nconst countBy: (values: readonly string[]) => readonly ICount[] = (values: readonly string[]): readonly ICount[] => {\n const counted: Map<string, number> = new Map();\n for (const value of values) {\n if (value) {\n counted.set(value, (counted.get(value) ?? 0) + 1);\n }\n }\n\n return [...counted]\n .map(([name, count]: [string, number]): ICount => ({ name, count }))\n .sort((left: ICount, right: ICount): number => right.count - left.count || left.name.localeCompare(right.name));\n};\n\nconst of: (observations: readonly IObservation[], event: TEvent) => readonly IObservation[] = (\n observations: readonly IObservation[],\n event: TEvent\n): readonly IObservation[] => observations.filter((entry: IObservation): boolean => entry.event === event);\n\n/**\n * Сводка за отрезок.\n *\n * `known` — имена правил, разложенных в это дерево. Без них сводка отвечает только на вопрос\n * «чем пользовались», а самое ценное — чем не пользовались ни разу — сказать нечем: в самих\n * наблюдениях незагруженного правила нет по определению.\n */\nexport function summarize(observations: readonly IObservation[], known: readonly string[], days: number): ISummary {\n const loads: readonly IObservation[] = of(observations, 'skill-load');\n const denials: readonly IObservation[] = of(observations, 'gate-deny');\n const loaded: Set<string> = new Set(loads.map((entry: IObservation): string => entry.resource));\n\n return {\n days,\n sessions: new Set(observations.map((entry: IObservation): string => entry.session).filter(Boolean)).size,\n loads: countBy(loads.map((entry: IObservation): string => entry.resource)),\n denials: countBy(denials.map((entry: IObservation): string => entry.resource)),\n kinds: countBy(denials.map((entry: IObservation): string => entry.kind)),\n guards: countBy(of(observations, 'guard-deny').map((entry: IObservation): string => entry.resource)),\n unused: known.filter((name: string): boolean => !loaded.has(name)).sort(byText),\n versions: [...new Set(observations.map((entry: IObservation): string => entry.version).filter(Boolean))].sort(byText),\n total: observations.length,\n };\n}\n"]}
package/lib/order.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Порядок строк, одинаковый на любой машине.
3
+ *
4
+ * Локаль названа прямо: взятая у среды, она разошлась бы между машинами, а порядок здесь уезжает
5
+ * в груз и в разложенные файлы — там расхождение читается как правка.
6
+ */
7
+ export declare function byText(one: string, other: string): number;
8
+ /** Порядок пар «ключ и значение» по ключу: карта своего порядка не держит. */
9
+ export declare function byKey(one: readonly [string, unknown], other: readonly [string, unknown]): number;
10
+ //# sourceMappingURL=order.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/order.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,8EAA8E;AAC9E,wBAAgB,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEhG"}
package/lib/order.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Порядок строк, одинаковый на любой машине.
3
+ *
4
+ * Локаль названа прямо: взятая у среды, она разошлась бы между машинами, а порядок здесь уезжает
5
+ * в груз и в разложенные файлы — там расхождение читается как правка.
6
+ */
7
+ export function byText(one, other) {
8
+ return one.localeCompare(other, 'en');
9
+ }
10
+ /** Порядок пар «ключ и значение» по ключу: карта своего порядка не держит. */
11
+ export function byKey(one, other) {
12
+ return byText(one[0], other[0]);
13
+ }
14
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/order.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,KAAa;IAC7C,OAAO,GAAG,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,GAA+B,EAAE,KAAiC;IACpF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * Порядок строк, одинаковый на любой машине.\n *\n * Локаль названа прямо: взятая у среды, она разошлась бы между машинами, а порядок здесь уезжает\n * в груз и в разложенные файлы — там расхождение читается как правка.\n */\nexport function byText(one: string, other: string): number {\n return one.localeCompare(other, 'en');\n}\n\n/** Порядок пар «ключ и значение» по ключу: карта своего порядка не держит. */\nexport function byKey(one: readonly [string, unknown], other: readonly [string, unknown]): number {\n return byText(one[0], other[0]);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/picker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,OAAO;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC/B;AAED,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,YAAY,CASlE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,YAAY,CASxE;AAUD,eAAO,MAAM,MAAM,EAAE,MAAsB,CAAC;AAC5C,eAAO,MAAM,QAAQ,EAAE,MAAsB,CAAC;AAC9C,eAAO,MAAM,UAAU,EAAE,MAAe,CAAC;AACzC,eAAO,MAAM,UAAU,EAAE,MAA+B,CAAC;AA8CzD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAcvD;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAwBvE;AAED,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,YACxB,CAAC;AAK1C,kGAAkG;AAClG,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAiBrF"}
1
+ {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/picker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,OAAO;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC/B;AAED,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,YAAY,CASlE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,YAAY,CASxE;AAUD,eAAO,MAAM,MAAM,EAAE,MAAsB,CAAC;AAC5C,eAAO,MAAM,QAAQ,EAAE,MAAsB,CAAC;AAC9C,eAAO,MAAM,UAAU,EAAE,MAAe,CAAC;AACzC,eAAO,MAAM,UAAU,EAAE,MAA+B,CAAC;AA8CzD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAcvD;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAwBvE;AAED,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,YACxB,CAAC;AAc1C,kGAAkG;AAClG,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAgBrF"}
package/lib/picker.js CHANGED
@@ -130,6 +130,13 @@ export function pressKey(state, key) {
130
130
  /** Кусок потока целиком: разобрать на клавиши и провести состояние через каждую. */
131
131
  export const pressChunk = (state, chunk) => keysOf(chunk).reduce(pressKey, state);
132
132
  const widest = (choices) => choices.reduce((width, choice) => Math.max(width, choice.name.length), 0);
133
+ /** Отметка выбора: одиночный выбор рисуется точкой в скобках, множественный — крестиком. */
134
+ function markOf(marked, single) {
135
+ if (single) {
136
+ return marked ? '(•)' : '( )';
137
+ }
138
+ return marked ? '[x]' : '[ ]';
139
+ }
133
140
  /** Разметка выбиралки строками. Курсор — стрелкой, выбранное — крестиком или точкой в скобках. */
134
141
  export function renderPicker(state, question) {
135
142
  const width = widest(state.choices);
@@ -141,8 +148,7 @@ export function renderPicker(state, question) {
141
148
  help,
142
149
  '',
143
150
  ...state.choices.map((choice, index) => {
144
- const marked = state.chosen.has(choice.id);
145
- const mark = state.single ? (marked ? '(•)' : '( )') : marked ? '[x]' : '[ ]';
151
+ const mark = markOf(state.chosen.has(choice.id), state.single);
146
152
  return `${index === state.cursor ? '❯' : ' '} ${mark} ${choice.name.padEnd(width)} ${choice.title}`;
147
153
  }),
148
154
  ];
package/lib/picker.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"picker.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/picker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Выбиралка галочками — её состояние и разметка, без единого касания терминала.\n *\n * Отделено от ввода-вывода по той же причине, что и команды: нажатие клавиши — это переход\n * состояния, и проверять его надо спекой, а не глазами на живом терминале. Сырой режим,\n * перерисовка и восстановление курсора живут на краю, в `bin/prompt.ts`, и не решают ничего.\n */\n\nexport interface IChoice {\n readonly id: string;\n /** Имя, которым ресурс называют в строке запуска: `access`. */\n readonly name: string;\n /** Заголовок ресурса — им человек и выбирает, имена ему ни о чём не говорят. */\n readonly title: string;\n}\n\nexport interface IPickerState {\n readonly choices: readonly IChoice[];\n readonly cursor: number;\n readonly chosen: ReadonlySet<string>;\n /**\n * Выбирается ровно один. Вид ресурса галочками не набирается: репозиторий лежит либо в\n * одном месте, либо в другом, и «оба» здесь означало бы два правила поставки в одном дереве.\n */\n readonly single: boolean;\n /** Выбор окончен, `chosen` — ответ. */\n readonly done: boolean;\n /** Выбор брошен: ответа нет, и подставлять вместо него умолчание нельзя. */\n readonly cancelled: boolean;\n}\n\n/** Первым предложением взято всё: отказ от закона — решение, и принимать его должен человек. */\nexport function pickerOf(choices: readonly IChoice[]): IPickerState {\n return {\n choices,\n cursor: 0,\n chosen: new Set(choices.map((choice: IChoice): string => choice.id)),\n single: false,\n done: false,\n cancelled: false,\n };\n}\n\n/**\n * Выбор одного из. Отмечено то, что под курсором, и переезд курсора переносит отметку: так\n * ответ есть в любой момент, и enter отвечает без единого нажатия пробела.\n */\nexport function singlePickerOf(choices: readonly IChoice[]): IPickerState {\n return {\n choices,\n cursor: 0,\n chosen: new Set(choices.slice(0, 1).map((choice: IChoice): string => choice.id)),\n single: true,\n done: false,\n cancelled: false,\n };\n}\n\n/**\n * Клавиши сырого потока. Записаны кодами, а не знаками: управляющий знак посреди исходника\n * невидим, и правка рядом с ним ломает разбор молча.\n *\n * Стрелка приходит целиком, тремя знаками, — сравнивать надо всю последовательность.\n */\nconst ESCAPE: string = String.fromCharCode(27);\n\nexport const KEY_UP: string = `${ESCAPE}[A`;\nexport const KEY_DOWN: string = `${ESCAPE}[B`;\nexport const KEY_ESCAPE: string = ESCAPE;\nexport const KEY_CANCEL: string = String.fromCharCode(3);\n\nconst UP: readonly string[] = [KEY_UP, 'k'];\nconst DOWN: readonly string[] = [KEY_DOWN, 'j'];\nconst DONE: readonly string[] = ['\\r', '\\n'];\n/** Ctrl-C и escape. Оба — «я передумал», и оба обязаны уйти без записи. */\nconst CANCEL: readonly string[] = [KEY_CANCEL, KEY_ESCAPE];\n\nfunction moved(state: IPickerState, step: number): IPickerState {\n const count: number = state.choices.length;\n if (!count) {\n return state;\n }\n const cursor: number = (state.cursor + step + count) % count;\n const chosen: ReadonlySet<string> = state.single ? new Set([state.choices[cursor].id]) : state.chosen;\n\n return { ...state, cursor, chosen };\n}\n\nfunction toggled(state: IPickerState): IPickerState {\n const choice: IChoice | undefined = state.choices[state.cursor];\n if (!choice) {\n return state;\n }\n // При выборе одного пробел ставит отметку, но не снимает: отметить нечего — ответа не\n // осталось бы вовсе, а брошенный выбор здесь говорится отдельной клавишей.\n if (state.single) {\n return { ...state, chosen: new Set([choice.id]) };\n }\n const chosen: Set<string> = new Set(state.chosen);\n if (!chosen.delete(choice.id)) {\n chosen.add(choice.id);\n }\n\n return { ...state, chosen };\n}\n\nfunction switched(state: IPickerState): IPickerState {\n if (state.single) {\n return state;\n }\n const all: boolean = state.chosen.size === state.choices.length;\n\n return { ...state, chosen: all ? new Set() : new Set(state.choices.map((choice: IChoice): string => choice.id)) };\n}\n\n/**\n * Кусок сырого потока, разобранный на клавиши.\n *\n * Терминал не обещает по куску на нажатие: набранное быстро, вставленное из буфера и всё, что\n * пришло по конвейеру, приезжает одной строкой. Неразобранный кусок читался бы как одна\n * незнакомая клавиша, и выбиралка молча не отзывалась бы ни на что — так она и повисла на\n * первом прогоне через конвейер.\n *\n * Escape-последовательность берётся целиком: `ESC [ A` — это стрелка, а не escape и две буквы.\n */\nexport function keysOf(chunk: string): readonly string[] {\n const points: readonly string[] = Array.from(chunk);\n const keys: string[] = [];\n\n for (let index: number = 0; index < points.length; index++) {\n if (points[index] === ESCAPE && points[index + 1] === '[' && points[index + 2] !== undefined) {\n keys.push(points.slice(index, index + 3).join(''));\n index += 2;\n } else {\n keys.push(points[index]);\n }\n }\n\n return keys;\n}\n\n/** Переход состояния по одной нажатой клавише. Незнакомая клавиша не делает ничего. */\nexport function pressKey(state: IPickerState, key: string): IPickerState {\n if (state.done || state.cancelled) {\n return state;\n }\n if (UP.includes(key)) {\n return moved(state, -1);\n }\n if (DOWN.includes(key)) {\n return moved(state, 1);\n }\n if (CANCEL.includes(key)) {\n return { ...state, cancelled: true };\n }\n if (DONE.includes(key)) {\n return { ...state, done: true };\n }\n if (key === ' ') {\n return toggled(state);\n }\n if (key === 'a') {\n return switched(state);\n }\n\n return state;\n}\n\n/** Кусок потока целиком: разобрать на клавиши и провести состояние через каждую. */\nexport const pressChunk: (state: IPickerState, chunk: string) => IPickerState = (state: IPickerState, chunk: string): IPickerState =>\n keysOf(chunk).reduce(pressKey, state);\n\nconst widest: (choices: readonly IChoice[]) => number = (choices: readonly IChoice[]): number =>\n choices.reduce((width: number, choice: IChoice): number => Math.max(width, choice.name.length), 0);\n\n/** Разметка выбиралки строками. Курсор — стрелкой, выбранное — крестиком или точкой в скобках. */\nexport function renderPicker(state: IPickerState, question: string): readonly string[] {\n const width: number = widest(state.choices);\n const help: string = state.single\n ? ' ↑↓ — двигаться, enter — готово'\n : ' ↑↓ — двигаться, пробел — выбрать, a — все или ни одного, enter — готово';\n\n return [\n question,\n help,\n '',\n ...state.choices.map((choice: IChoice, index: number): string => {\n const marked: boolean = state.chosen.has(choice.id);\n const mark: string = state.single ? (marked ? '(•)' : '( )') : marked ? '[x]' : '[ ]';\n\n return `${index === state.cursor ? '❯' : ' '} ${mark} ${choice.name.padEnd(width)} ${choice.title}`;\n }),\n ];\n}\n"]}
1
+ {"version":3,"file":"picker.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/picker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Выбиралка галочками — её состояние и разметка, без единого касания терминала.\n *\n * Отделено от ввода-вывода по той же причине, что и команды: нажатие клавиши — это переход\n * состояния, и проверять его надо спекой, а не глазами на живом терминале. Сырой режим,\n * перерисовка и восстановление курсора живут на краю, в `bin/prompt.ts`, и не решают ничего.\n */\n\nexport interface IChoice {\n readonly id: string;\n /** Имя, которым ресурс называют в строке запуска: `access`. */\n readonly name: string;\n /** Заголовок ресурса — им человек и выбирает, имена ему ни о чём не говорят. */\n readonly title: string;\n}\n\nexport interface IPickerState {\n readonly choices: readonly IChoice[];\n readonly cursor: number;\n readonly chosen: ReadonlySet<string>;\n /**\n * Выбирается ровно один. Вид ресурса галочками не набирается: репозиторий лежит либо в\n * одном месте, либо в другом, и «оба» здесь означало бы два правила поставки в одном дереве.\n */\n readonly single: boolean;\n /** Выбор окончен, `chosen` — ответ. */\n readonly done: boolean;\n /** Выбор брошен: ответа нет, и подставлять вместо него умолчание нельзя. */\n readonly cancelled: boolean;\n}\n\n/** Первым предложением взято всё: отказ от закона — решение, и принимать его должен человек. */\nexport function pickerOf(choices: readonly IChoice[]): IPickerState {\n return {\n choices,\n cursor: 0,\n chosen: new Set(choices.map((choice: IChoice): string => choice.id)),\n single: false,\n done: false,\n cancelled: false,\n };\n}\n\n/**\n * Выбор одного из. Отмечено то, что под курсором, и переезд курсора переносит отметку: так\n * ответ есть в любой момент, и enter отвечает без единого нажатия пробела.\n */\nexport function singlePickerOf(choices: readonly IChoice[]): IPickerState {\n return {\n choices,\n cursor: 0,\n chosen: new Set(choices.slice(0, 1).map((choice: IChoice): string => choice.id)),\n single: true,\n done: false,\n cancelled: false,\n };\n}\n\n/**\n * Клавиши сырого потока. Записаны кодами, а не знаками: управляющий знак посреди исходника\n * невидим, и правка рядом с ним ломает разбор молча.\n *\n * Стрелка приходит целиком, тремя знаками, — сравнивать надо всю последовательность.\n */\nconst ESCAPE: string = String.fromCharCode(27);\n\nexport const KEY_UP: string = `${ESCAPE}[A`;\nexport const KEY_DOWN: string = `${ESCAPE}[B`;\nexport const KEY_ESCAPE: string = ESCAPE;\nexport const KEY_CANCEL: string = String.fromCharCode(3);\n\nconst UP: readonly string[] = [KEY_UP, 'k'];\nconst DOWN: readonly string[] = [KEY_DOWN, 'j'];\nconst DONE: readonly string[] = ['\\r', '\\n'];\n/** Ctrl-C и escape. Оба — «я передумал», и оба обязаны уйти без записи. */\nconst CANCEL: readonly string[] = [KEY_CANCEL, KEY_ESCAPE];\n\nfunction moved(state: IPickerState, step: number): IPickerState {\n const count: number = state.choices.length;\n if (!count) {\n return state;\n }\n const cursor: number = (state.cursor + step + count) % count;\n const chosen: ReadonlySet<string> = state.single ? new Set([state.choices[cursor].id]) : state.chosen;\n\n return { ...state, cursor, chosen };\n}\n\nfunction toggled(state: IPickerState): IPickerState {\n const choice: IChoice | undefined = state.choices[state.cursor];\n if (!choice) {\n return state;\n }\n // При выборе одного пробел ставит отметку, но не снимает: отметить нечего — ответа не\n // осталось бы вовсе, а брошенный выбор здесь говорится отдельной клавишей.\n if (state.single) {\n return { ...state, chosen: new Set([choice.id]) };\n }\n const chosen: Set<string> = new Set(state.chosen);\n if (!chosen.delete(choice.id)) {\n chosen.add(choice.id);\n }\n\n return { ...state, chosen };\n}\n\nfunction switched(state: IPickerState): IPickerState {\n if (state.single) {\n return state;\n }\n const all: boolean = state.chosen.size === state.choices.length;\n\n return { ...state, chosen: all ? new Set() : new Set(state.choices.map((choice: IChoice): string => choice.id)) };\n}\n\n/**\n * Кусок сырого потока, разобранный на клавиши.\n *\n * Терминал не обещает по куску на нажатие: набранное быстро, вставленное из буфера и всё, что\n * пришло по конвейеру, приезжает одной строкой. Неразобранный кусок читался бы как одна\n * незнакомая клавиша, и выбиралка молча не отзывалась бы ни на что — так она и повисла на\n * первом прогоне через конвейер.\n *\n * Escape-последовательность берётся целиком: `ESC [ A` — это стрелка, а не escape и две буквы.\n */\nexport function keysOf(chunk: string): readonly string[] {\n const points: readonly string[] = Array.from(chunk);\n const keys: string[] = [];\n\n for (let index: number = 0; index < points.length; index++) {\n if (points[index] === ESCAPE && points[index + 1] === '[' && points[index + 2] !== undefined) {\n keys.push(points.slice(index, index + 3).join(''));\n index += 2;\n } else {\n keys.push(points[index]);\n }\n }\n\n return keys;\n}\n\n/** Переход состояния по одной нажатой клавише. Незнакомая клавиша не делает ничего. */\nexport function pressKey(state: IPickerState, key: string): IPickerState {\n if (state.done || state.cancelled) {\n return state;\n }\n if (UP.includes(key)) {\n return moved(state, -1);\n }\n if (DOWN.includes(key)) {\n return moved(state, 1);\n }\n if (CANCEL.includes(key)) {\n return { ...state, cancelled: true };\n }\n if (DONE.includes(key)) {\n return { ...state, done: true };\n }\n if (key === ' ') {\n return toggled(state);\n }\n if (key === 'a') {\n return switched(state);\n }\n\n return state;\n}\n\n/** Кусок потока целиком: разобрать на клавиши и провести состояние через каждую. */\nexport const pressChunk: (state: IPickerState, chunk: string) => IPickerState = (state: IPickerState, chunk: string): IPickerState =>\n keysOf(chunk).reduce(pressKey, state);\n\nconst widest: (choices: readonly IChoice[]) => number = (choices: readonly IChoice[]): number =>\n choices.reduce((width: number, choice: IChoice): number => Math.max(width, choice.name.length), 0);\n\n/** Отметка выбора: одиночный выбор рисуется точкой в скобках, множественный — крестиком. */\nfunction markOf(marked: boolean, single: boolean): string {\n if (single) {\n return marked ? '(•)' : '( )';\n }\n\n return marked ? '[x]' : '[ ]';\n}\n\n/** Разметка выбиралки строками. Курсор — стрелкой, выбранное — крестиком или точкой в скобках. */\nexport function renderPicker(state: IPickerState, question: string): readonly string[] {\n const width: number = widest(state.choices);\n const help: string = state.single\n ? ' ↑↓ — двигаться, enter — готово'\n : ' ↑↓ — двигаться, пробел — выбрать, a — все или ни одного, enter — готово';\n\n return [\n question,\n help,\n '',\n ...state.choices.map((choice: IChoice, index: number): string => {\n const mark: string = markOf(state.chosen.has(choice.id), state.single);\n\n return `${index === state.cursor ? '❯' : ' '} ${mark} ${choice.name.padEnd(width)} ${choice.title}`;\n }),\n ];\n}\n"]}
package/lib/plan.js CHANGED
@@ -10,9 +10,9 @@ export function planFile(input) {
10
10
  const stamp = { version: input.version, asset: input.asset, hash: digestOf(input.rendered) };
11
11
  const content = applyStamp(input.rendered, stamp, input.path);
12
12
  const planned = (outcome, write) => ({
13
+ outcome,
13
14
  path: input.path,
14
15
  asset: input.asset,
15
- outcome,
16
16
  content: write ? content : null,
17
17
  });
18
18
  if (input.existing === null) {