@rt-tools/agent-kit 0.10.0 → 0.12.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 (139) hide show
  1. package/assets/checks/check-file-size.mjs +19 -4
  2. package/assets/checks/check-state-next.mjs +202 -0
  3. package/assets/checks/check-states.mjs +142 -0
  4. package/assets/checks/check-turn-map.mjs +146 -0
  5. package/assets/checks/rt-kit-checks.config.mjs +16 -2
  6. package/assets/defaults/project.sh +67 -7
  7. package/assets/defaults/turn-map.md +48 -0
  8. package/assets/hooks/browser-device-id.sh +2 -0
  9. package/assets/hooks/browser-guard-device-id.sh +5 -1
  10. package/assets/hooks/browser-guard-no-asking.sh +5 -1
  11. package/assets/hooks/browser-guard-no-listing.sh +2 -0
  12. package/assets/hooks/browser-guard-no-other-drivers.sh +7 -3
  13. package/assets/hooks/browser-guard-require-select.sh +6 -2
  14. package/assets/hooks/claim-guard.sh +5 -1
  15. package/assets/hooks/commit-msg.sh +2 -0
  16. package/assets/hooks/conscience-guard.sh +5 -1
  17. package/assets/hooks/constitution-index.sh +2 -0
  18. package/assets/hooks/dev-server-guard.sh +7 -3
  19. package/assets/hooks/dispatch.sh +69 -0
  20. package/assets/hooks/docs-guard.sh +8 -4
  21. package/assets/hooks/exam-guard.sh +7 -3
  22. package/assets/hooks/git-guard-delivery-signature.sh +2 -0
  23. package/assets/hooks/git-guard-delivery.sh +39 -5
  24. package/assets/hooks/git-guard-main.sh +8 -4
  25. package/assets/hooks/git-guard-push-tests.sh +8 -4
  26. package/assets/hooks/glossary-load.sh +2 -0
  27. package/assets/hooks/grill-gate.sh +6 -2
  28. package/assets/hooks/handoff-entry-guard.sh +6 -2
  29. package/assets/hooks/handoff-write.sh +124 -0
  30. package/assets/hooks/hook-input.sh +54 -0
  31. package/assets/hooks/lint-after-edit.sh +7 -3
  32. package/assets/hooks/observe.sh +2 -0
  33. package/assets/hooks/postmortem-guard.sh +5 -1
  34. package/assets/hooks/proposal-guard.sh +5 -1
  35. package/assets/hooks/prose-style-guard.sh +7 -3
  36. package/assets/hooks/qa-dataid-guard.sh +6 -2
  37. package/assets/hooks/rerun-guard.sh +7 -3
  38. package/assets/hooks/reuse-first-guard.sh +7 -3
  39. package/assets/hooks/roles.sh +2 -0
  40. package/assets/hooks/rule-article.sh +99 -0
  41. package/assets/hooks/skill-gate-layers.sh +2 -0
  42. package/assets/hooks/skill-gate-rearm.sh +5 -1
  43. package/assets/hooks/skill-gate.sh +25 -2
  44. package/assets/hooks/skill-loaded.sh +5 -1
  45. package/assets/hooks/sql-guard-parse.sh +2 -0
  46. package/assets/hooks/sql-guard-request.sh +4 -1
  47. package/assets/hooks/sql-guard-target.sh +2 -0
  48. package/assets/hooks/sql-guard-write.sh +2 -0
  49. package/assets/hooks/sql-guard.sh +6 -2
  50. package/assets/hooks/task-context-load.sh +2 -0
  51. package/assets/hooks/task-flow-guard.sh +8 -4
  52. package/assets/hooks/turn-entry-load.sh +62 -0
  53. package/assets/hooks/turn-exit-guard.sh +44 -17
  54. package/assets/hooks/utf8.sh +35 -0
  55. package/assets/hooks/waiting-turn-guard.sh +5 -1
  56. package/assets/hooks/window-fill-guard.sh +37 -5
  57. package/assets/laws/work-conduct.md +34 -9
  58. package/assets/patterns/dependencies-upgrade.md +1 -1
  59. package/assets/patterns/doc-style-write.md +3 -3
  60. package/assets/patterns/git-workflow-commit.azure.md +2 -202
  61. package/assets/patterns/git-workflow-commit.github.md +2 -258
  62. package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
  63. package/assets/patterns/git-workflow-docker.md +3 -3
  64. package/assets/patterns/git-workflow-merge.md +3 -2
  65. package/assets/patterns/git-workflow-migration.md +3 -3
  66. package/assets/patterns/git-workflow-pr.azure.md +224 -0
  67. package/assets/patterns/git-workflow-pr.github.md +280 -0
  68. package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
  69. package/assets/patterns/git-workflow-restart.md +3 -3
  70. package/assets/patterns/git-workflow-secrets.md +3 -3
  71. package/assets/patterns/task-flow-archive.md +193 -0
  72. package/assets/patterns/task-flow-close.md +11 -160
  73. package/assets/patterns/task-flow-handoff.md +22 -4
  74. package/assets/patterns/task-flow-resume.md +6 -0
  75. package/assets/patterns/task-flow-start.md +41 -0
  76. package/assets/patterns/turn-entry-map.md +81 -0
  77. package/assets/pitfalls/doc-style.md +80 -0
  78. package/assets/pitfalls/git-workflow.azure.md +50 -0
  79. package/assets/pitfalls/git-workflow.github.md +78 -0
  80. package/assets/pitfalls/git-workflow.gitlab.md +49 -0
  81. package/assets/pitfalls/spec-driven.md +36 -0
  82. package/assets/pitfalls/styling-bem.md +45 -0
  83. package/assets/pitfalls/task-flow.md +62 -0
  84. package/assets/pitfalls/testing.md +70 -0
  85. package/assets/rules/deploy-flow.azure.md +106 -0
  86. package/assets/rules/deploy-flow.github.md +113 -0
  87. package/assets/rules/deploy-flow.gitlab.md +108 -0
  88. package/assets/rules/doc-style.md +25 -76
  89. package/assets/rules/git-workflow.azure.md +6 -92
  90. package/assets/rules/git-workflow.github.md +14 -127
  91. package/assets/rules/git-workflow.gitlab.md +6 -93
  92. package/assets/rules/spec-driven.md +39 -30
  93. package/assets/rules/styling-bem.md +20 -39
  94. package/assets/rules/task-flow.md +17 -186
  95. package/assets/rules/testing.md +3 -64
  96. package/assets/rules/turn-conduct.md +206 -0
  97. package/assets/rules/turn-entry.md +93 -0
  98. package/assets/rules/typescript-conventions.md +15 -0
  99. package/assets/skills/agent-kit.md +35 -12
  100. package/assets/templates/pitfalls.md +10 -0
  101. package/assets/templates/rule.md +5 -3
  102. package/lib/assets.d.ts.map +1 -1
  103. package/lib/assets.js +6 -1
  104. package/lib/assets.js.map +1 -1
  105. package/lib/cargo.d.ts +42 -0
  106. package/lib/cargo.d.ts.map +1 -1
  107. package/lib/cargo.js +2 -0
  108. package/lib/cargo.js.map +1 -1
  109. package/lib/cascade.d.ts.map +1 -1
  110. package/lib/cascade.js +19 -1
  111. package/lib/cascade.js.map +1 -1
  112. package/lib/commands.d.ts.map +1 -1
  113. package/lib/commands.js +65 -4
  114. package/lib/commands.js.map +1 -1
  115. package/lib/config.d.ts +16 -1
  116. package/lib/config.d.ts.map +1 -1
  117. package/lib/config.js +8 -0
  118. package/lib/config.js.map +1 -1
  119. package/lib/hooks-map.d.ts +13 -0
  120. package/lib/hooks-map.d.ts.map +1 -1
  121. package/lib/hooks-map.js +33 -1
  122. package/lib/hooks-map.js.map +1 -1
  123. package/lib/observations.d.ts +35 -1
  124. package/lib/observations.d.ts.map +1 -1
  125. package/lib/observations.js +14 -2
  126. package/lib/observations.js.map +1 -1
  127. package/lib/ship.d.ts +2 -0
  128. package/lib/ship.d.ts.map +1 -1
  129. package/lib/ship.js +2 -0
  130. package/lib/ship.js.map +1 -1
  131. package/lib/thresholds.d.ts +49 -0
  132. package/lib/thresholds.d.ts.map +1 -0
  133. package/lib/thresholds.js +151 -0
  134. package/lib/thresholds.js.map +1 -0
  135. package/package.json +1 -1
  136. package/rt-tools-agent-kit-0.12.0.tgz +0 -0
  137. package/assets/commands/agent-kit-digest.md +0 -88
  138. package/assets/commands/rules-review.md +0 -98
  139. package/rt-tools-agent-kit-0.10.0.tgz +0 -0
@@ -35,8 +35,18 @@ import { join } from 'node:path';
35
35
  import { allowlistOf, baselineOf, CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
36
36
 
37
37
  const ALLOWLIST = allowlistOf('file-size');
38
- /** Предел один на все роды файлов: своё число каждому роду спор о числе на каждой правке. */
38
+ /** Пределов два: код и текст слоя правил. Какой из них применён, каждая строка отказа называет. */
39
39
  const LIMIT = CONFIG.fileSizeLimit;
40
+ const PROSE_LIMIT = CONFIG.proseSizeLimit ?? CONFIG.fileSizeLimit;
41
+ /** Корни текста слоя правил; дерево, их не назвавшее, судится одним пределом. */
42
+ const PROSE_ROOTS = CONFIG.proseRoots ?? [];
43
+
44
+ /** Предел для файла и имя предела для отказа: по корню, а не по расширению — код лежит и в `.md`. */
45
+ function limitOf(path) {
46
+ return PROSE_ROOTS.some((root) => root && path.startsWith(root))
47
+ ? { limit: PROSE_LIMIT, title: 'предел текста' }
48
+ : { limit: LIMIT, title: 'предел кода' };
49
+ }
40
50
 
41
51
  /** Роды файлов, которых не читает линтер. Код остаётся за ним. */
42
52
  const JUDGED = ['.md', '.scss', '.html', '.js', '.mjs', '.sh'];
@@ -78,7 +88,7 @@ const tracked = trackedFiles().filter(judged);
78
88
 
79
89
  for (const path of tracked) {
80
90
  const lines = lineCount(path);
81
- if (lines > LIMIT) {
91
+ if (lines > limitOf(path).limit) {
82
92
  tooLong.set(path, lines);
83
93
  }
84
94
  }
@@ -95,7 +105,10 @@ const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
95
105
  const shrunk = [...known.keys()].filter((path) => !tooLong.has(path) && existsSync(join(ROOT, path)));
96
106
 
97
107
  const problems = [
98
- ...fresh.map(([path, lines]) => `${path}: ${lines} строк, предел ${LIMIT} — делить, а не дописывать строку в ${ALLOWLIST}`),
108
+ ...fresh.map(([path, lines]) => {
109
+ const { limit, title } = limitOf(path);
110
+ return `${path}: ${lines} строк, ${title} ${limit} — делить, а не дописывать строку в ${ALLOWLIST}`;
111
+ }),
99
112
  ...gone.map((path) => `${path}: строка в ${ALLOWLIST} устарела — файла в дереве нет`),
100
113
  ...shrunk.map((path) => `${path}: значится в ${ALLOWLIST}, но уже короче предела — строку убрать`),
101
114
  ];
@@ -107,7 +120,9 @@ if (problems.length > 0) {
107
120
  process.exit(1);
108
121
  }
109
122
 
123
+ const limits = PROSE_ROOTS.length > 0 ? `предел кода ${LIMIT}, предел текста ${PROSE_LIMIT}` : `предел ${LIMIT}`;
124
+
110
125
  console.log(
111
- `check-file-size: проверено ${tracked.length} файлов, длиннее ${LIMIT} строк ${tooLong.size}, ` +
126
+ `check-file-size: проверено ${tracked.length} файлов, ${limits}, длиннее предела ${tooLong.size}, ` +
112
127
  `из них принято ${accepted.size}, долг ${debt.size} — новых нет`
113
128
  );
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка того, что раздел состояния называет следующее движение.
4
+ *
5
+ * Раздел, обрывающийся на последнем приёме, читается как конец работы: исполнитель
6
+ * доводит обязательное действие до конца, дочитывает раздел, следующего движения в
7
+ * нём не находит — и отдаёт ход отчётом о сделанном. Так кончились два хода: один
8
+ * на записанном замысле, второй на закрытом разборе просьбы. Сверка состояний этого
9
+ * не видит: она сравнивает имена разделов с перечнем и внутрь не смотрит.
10
+ *
11
+ * Читаются четыре текста:
12
+ * перечень — таблица состояний в правиле ведения работы: имя и ведущий паттерн;
13
+ * разделы — заголовки вида «Состояние `имя`» в этих паттернах;
14
+ * правило — оно же: в нём стоит утверждение о границе состояния;
15
+ * карта и закон — там же стоит то же утверждение, если дерево их разложило.
16
+ *
17
+ * Строка следующего движения узнаётся по зачину, а называет своё: единая дословная
18
+ * строка читается как шаблон и перестаёт замечаться на третьем разделе. Поэтому
19
+ * повтор хвоста считается расхождением наравне с его отсутствием.
20
+ *
21
+ * FAIL-OPEN: правила ведения работы в дереве нет — сверять нечего, нулевой код.
22
+ * Пустая таблица состояний отказом считается: она означает не «нечего сверять», а
23
+ * «перечень сломан». Ведущий паттерн, которого в дереве нет, здесь не судится — его
24
+ * называет сверка состояний, и два отказа об одном промахе читаются как две
25
+ * претензии.
26
+ *
27
+ * Ненулевой код возврата и перечень расхождений.
28
+ */
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+
32
+ const ROOT = process.cwd();
33
+ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
34
+ /**
35
+ * Правило хода захода: граница состояния живёт там, а не в правиле ведения работы.
36
+ * Разделились они, когда правило ведения работы вышло за предел длины: ход работы
37
+ * остался в одном, выходы хода уехали в другое. Дерево, где разложено только первое,
38
+ * судится по нему одному — второго файла у него нет.
39
+ */
40
+ const TURN_RULE = join(ROOT, '.claude/skills/turn-conduct/SKILL.md');
41
+ const SKILLS = join(ROOT, '.claude/skills');
42
+ const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
43
+ const LAW = join(ROOT, 'docs/constitution/work-conduct.md');
44
+
45
+ /** Зачин строки: по нему её находят, а хвост у каждого раздела свой. */
46
+ const MARKER = '**Следующее движение:**';
47
+
48
+ /** Утверждение о границе состояния. Стоит в правиле, в карте и в законе теми же словами. */
49
+ const BOUNDARY = 'Переход из состояния в состояние';
50
+
51
+ /** Короче этого хвост движения не называет: зачин без движения — та же пустота. */
52
+ const MIN_TAIL = 20;
53
+
54
+ /** Имя состояния и паттерн, который его ведёт, — из таблицы правила. */
55
+ function statesFromRule(text) {
56
+ const states = [];
57
+
58
+ for (const line of text.split('\n')) {
59
+ if (!line.startsWith('|')) {
60
+ continue;
61
+ }
62
+
63
+ const cells = line
64
+ .split('|')
65
+ .slice(1, -1)
66
+ .map((cell) => cell.trim());
67
+
68
+ if (cells.length < 4) {
69
+ continue;
70
+ }
71
+
72
+ const name = cells[0].match(/^`([^`]+)`$/);
73
+ const pattern = cells[3].match(/^`([^`]+)`$/);
74
+
75
+ if (name && pattern) {
76
+ states.push({ name: name[1], pattern: pattern[1] });
77
+ }
78
+ }
79
+
80
+ return states;
81
+ }
82
+
83
+ /** Разделы состояний паттерна: имя, заголовок целиком и найденные в разделе строки движения. */
84
+ function sectionsOf(pattern) {
85
+ const file = join(SKILLS, pattern, 'SKILL.md');
86
+
87
+ if (!existsSync(file)) {
88
+ return null;
89
+ }
90
+
91
+ const sections = [];
92
+ let current = null;
93
+
94
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
95
+ const heading = line.match(/^#+\s+Состояние\s+`([^`]+)`/);
96
+
97
+ if (heading) {
98
+ current = { name: heading[1], heading: line.replace(/^#+\s+/, ''), moves: [] };
99
+ sections.push(current);
100
+ continue;
101
+ }
102
+
103
+ if (current && line.startsWith(MARKER)) {
104
+ current.moves.push(line.slice(MARKER.length).trim());
105
+ }
106
+ }
107
+
108
+ return sections;
109
+ }
110
+
111
+ if (!existsSync(RULE)) {
112
+ console.log('check-state-next: правила ведения работы в дереве нет — сверять нечего');
113
+ process.exit(0);
114
+ }
115
+
116
+ const ruleText = readFileSync(RULE, 'utf8');
117
+ const states = statesFromRule(ruleText);
118
+
119
+ if (states.length === 0) {
120
+ console.error('check-state-next: в правиле ведения работы не нашлось таблицы состояний');
121
+ process.exit(1);
122
+ }
123
+
124
+ const problems = [];
125
+ const patterns = new Map();
126
+ const tails = new Map();
127
+ let counted = 0;
128
+
129
+ for (const state of states) {
130
+ if (!patterns.has(state.pattern)) {
131
+ patterns.set(state.pattern, sectionsOf(state.pattern));
132
+ }
133
+ }
134
+
135
+ for (const [pattern, sections] of patterns) {
136
+ if (sections === null) {
137
+ continue;
138
+ }
139
+
140
+ for (const section of sections) {
141
+ counted += 1;
142
+
143
+ const where = `\`${section.name}\` в паттерне \`${pattern}\``;
144
+
145
+ if (section.moves.length === 0) {
146
+ problems.push(`${where}: в разделе «${section.heading}» нет строки «${MARKER}»`);
147
+ continue;
148
+ }
149
+
150
+ if (section.moves.length > 1) {
151
+ problems.push(`${where}: в разделе «${section.heading}» таких строк ${section.moves.length}, а движение одно`);
152
+ continue;
153
+ }
154
+
155
+ const tail = section.moves[0];
156
+
157
+ if (tail.length < MIN_TAIL) {
158
+ problems.push(`${where}: зачин есть, а движение за ним не названо`);
159
+ continue;
160
+ }
161
+
162
+ const twin = tails.get(tail);
163
+
164
+ if (twin) {
165
+ problems.push(`${where}: движение слово в слово то же, что у ${twin}`);
166
+ continue;
167
+ }
168
+
169
+ tails.set(tail, `\`${section.name}\` в паттерне \`${pattern}\``);
170
+ }
171
+ }
172
+
173
+ if (counted === 0) {
174
+ problems.push('ни одного раздела состояния не нашлось: паттерны не разложены или заголовки в них другие');
175
+ }
176
+
177
+ const turnText = existsSync(TURN_RULE) ? readFileSync(TURN_RULE, 'utf8') : '';
178
+ if (!ruleText.includes(BOUNDARY) && !turnText.includes(BOUNDARY)) {
179
+ problems.push(`правило хода захода о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
180
+ }
181
+
182
+ for (const [file, what] of [
183
+ [MAP, 'карта хода'],
184
+ [LAW, 'закон о ведении работы'],
185
+ ]) {
186
+ if (existsSync(file) && !readFileSync(file, 'utf8').includes(BOUNDARY)) {
187
+ problems.push(`${what} о границе состояния молчит: строки «${BOUNDARY}» в ней нет`);
188
+ }
189
+ }
190
+
191
+ if (problems.length > 0) {
192
+ console.error(`check-state-next: расхождений ${problems.length}`);
193
+
194
+ for (const problem of problems) {
195
+ console.error(` ${problem}`);
196
+ }
197
+
198
+ console.error('\nСтрока следующего движения стоит в каждом разделе состояния: зачин общий, движение своё.');
199
+ process.exit(1);
200
+ }
201
+
202
+ console.log(`check-state-next: разделов состояния ${counted}, у каждого названо следующее движение`);
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка перечня состояний работы с разделами паттернов, которые их ведут.
4
+ *
5
+ * Состояние без ведущего текста читается как конец работы: исполнитель дочитывает
6
+ * паттерн до последнего раздела, следующего движения в нём нет, и ход кончается
7
+ * отчётом. Так простояло состояние «замысел записан» — оно встречалось один раз,
8
+ * клеткой таблицы, и дыру нашло происшествие, а не проверка. Разбор —
9
+ * `docs/postmortems/2026-08-21-turn-ended-at-the-written-plan.md`.
10
+ *
11
+ * Сверяются два множества в обе стороны:
12
+ * перечень — таблица состояний в правиле ведения работы: имя и ведущий паттерн;
13
+ * разделы — заголовки вида «Состояние `имя`» в паттернах.
14
+ *
15
+ * Состояние без раздела — расхождение. Раздел про состояние вне перечня — тоже:
16
+ * переименованное состояние иначе оставляет прежний раздел, и тот выглядит
17
+ * действующим.
18
+ *
19
+ * Ведущий паттерн берётся из самой таблицы, а не из имён файлов: раздел, лежащий
20
+ * не в том паттерне, который назначен, читателя до себя не доводит — он открывает
21
+ * назначенный.
22
+ *
23
+ * FAIL-OPEN: правила ведения работы в дереве нет — сверять нечего, нулевой код.
24
+ * Пустая таблица состояний отказом считается: она означает не «нечего сверять», а
25
+ * «перечень сломан».
26
+ *
27
+ * Ненулевой код возврата и перечень расхождений.
28
+ */
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+
32
+ const ROOT = process.cwd();
33
+ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
34
+ const SKILLS = join(ROOT, '.claude/skills');
35
+
36
+ /** Имя состояния и паттерн, который его ведёт, — из таблицы правила. */
37
+ function statesFromRule(text) {
38
+ const states = [];
39
+
40
+ for (const line of text.split('\n')) {
41
+ if (!line.startsWith('|')) {
42
+ continue;
43
+ }
44
+
45
+ const cells = line
46
+ .split('|')
47
+ .slice(1, -1)
48
+ .map((cell) => cell.trim());
49
+
50
+ if (cells.length < 4) {
51
+ continue;
52
+ }
53
+
54
+ const name = cells[0].match(/^`([^`]+)`$/);
55
+ const pattern = cells[3].match(/^`([^`]+)`$/);
56
+
57
+ if (name && pattern) {
58
+ states.push({ name: name[1], pattern: pattern[1] });
59
+ }
60
+ }
61
+
62
+ return states;
63
+ }
64
+
65
+ /** Состояния, про которые в паттерне есть раздел. Заголовок любого уровня. */
66
+ function sectionsOf(pattern) {
67
+ const file = join(SKILLS, pattern, 'SKILL.md');
68
+
69
+ if (!existsSync(file)) {
70
+ return null;
71
+ }
72
+
73
+ const found = new Set();
74
+
75
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
76
+ const heading = line.match(/^#+\s+Состояние\s+`([^`]+)`/);
77
+
78
+ if (heading) {
79
+ found.add(heading[1]);
80
+ }
81
+ }
82
+
83
+ return found;
84
+ }
85
+
86
+ if (!existsSync(RULE)) {
87
+ console.log('check-states: правила ведения работы в дереве нет — сверять нечего');
88
+ process.exit(0);
89
+ }
90
+
91
+ const states = statesFromRule(readFileSync(RULE, 'utf8'));
92
+
93
+ if (states.length === 0) {
94
+ console.error('check-states: в правиле ведения работы не нашлось таблицы состояний');
95
+ process.exit(1);
96
+ }
97
+
98
+ const known = new Set(states.map((state) => state.name));
99
+ const problems = [];
100
+ const seen = new Map();
101
+
102
+ for (const state of states) {
103
+ if (!seen.has(state.pattern)) {
104
+ seen.set(state.pattern, sectionsOf(state.pattern));
105
+ }
106
+
107
+ const sections = seen.get(state.pattern);
108
+
109
+ if (sections === null) {
110
+ problems.push(`состояние \`${state.name}\`: ведущий паттерн \`${state.pattern}\` в дереве не разложен`);
111
+ continue;
112
+ }
113
+
114
+ if (!sections.has(state.name)) {
115
+ problems.push(`состояние \`${state.name}\`: в паттерне \`${state.pattern}\` нет раздела «Состояние \`${state.name}\`»`);
116
+ }
117
+ }
118
+
119
+ for (const [pattern, sections] of seen) {
120
+ if (sections === null) {
121
+ continue;
122
+ }
123
+
124
+ for (const name of sections) {
125
+ if (!known.has(name)) {
126
+ problems.push(`паттерн \`${pattern}\`: раздел про \`${name}\`, а такого состояния в перечне нет`);
127
+ }
128
+ }
129
+ }
130
+
131
+ if (problems.length > 0) {
132
+ console.error(`check-states: расхождений ${problems.length}`);
133
+
134
+ for (const problem of problems) {
135
+ console.error(` ${problem}`);
136
+ }
137
+
138
+ console.error('\nПеречень состояний — правило `task-flow`, разделы — паттерны при нём.');
139
+ process.exit(1);
140
+ }
141
+
142
+ console.log(`check-states: состояний ${states.length}, у каждого есть раздел в ведущем паттерне`);
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка карты хода: её размер и её полнота.
4
+ *
5
+ * Карту кладёт в контекст хук входа в заход — целиком, до первой реплики. Тем она и
6
+ * отличается от правила: правило исполнитель читает сам и платит за это ходом, карта
7
+ * приходит даром. Даром — пока она короткая: карта, выросшая до правила, съедает то
8
+ * самое окно, ради которого её и заводили, и заметить это нечем — она продолжает
9
+ * приходить и продолжает быть верной.
10
+ *
11
+ * Второе, чего не видно без сверки: состояние, заведённое в правиле и забытое в карте.
12
+ * Заход тогда получает карту, не находит в ней своего состояния и идёт читать правило —
13
+ * то есть карта работает ровно до первого нового состояния.
14
+ *
15
+ * Сверяются три вещи:
16
+ * размер — байты разложенной карты против объявленного предела;
17
+ * состояния — имена из таблицы правила против имён в карте, в обе стороны;
18
+ * выходы — все четыре выхода хода названы.
19
+ *
20
+ * FAIL-OPEN: карты в дереве нет — сверять нечего, нулевой код. Дерево может её не
21
+ * раскладывать. Пустая таблица состояний в карте отказом считается: это не «нечего
22
+ * сверять», а «карта сломана».
23
+ *
24
+ * Ненулевой код возврата и перечень расхождений.
25
+ */
26
+ import { existsSync, readFileSync, statSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+
29
+ const ROOT = process.cwd();
30
+ const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
31
+ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
32
+
33
+ /**
34
+ * Предел размера карты в байтах.
35
+ *
36
+ * Число одно и живёт здесь: карта приходит в каждый заход целиком, и предел ей — свойство
37
+ * приёма, а не дерева. Шесть килобайт взяты замером, а не на глаз: карта в день заведения
38
+ * весит 4495 байт, правило, из которого она выжата, — 63803. Запас в полтора килобайта —
39
+ * это два-три новых состояния; выйдя за него, карта перестаёт быть выжимкой, и делить её
40
+ * тогда надо, а не поднимать предел.
41
+ */
42
+ const LIMIT_BYTES = 6144;
43
+
44
+ /** Имена состояний из таблицы: первая ячейка в обратных кавычках и всё, что за ней. */
45
+ function statesOf(text) {
46
+ const states = [];
47
+
48
+ for (const line of text.split('\n')) {
49
+ if (!line.startsWith('|')) {
50
+ continue;
51
+ }
52
+
53
+ const cells = line
54
+ .split('|')
55
+ .slice(1, -1)
56
+ .map((cell) => cell.trim());
57
+
58
+ if (cells.length < 2) {
59
+ continue;
60
+ }
61
+
62
+ const name = cells[0].match(/^`([^`]+)`$/);
63
+
64
+ if (name) {
65
+ states.push({ name: name[1], rest: cells.slice(1) });
66
+ }
67
+ }
68
+
69
+ return states;
70
+ }
71
+
72
+ /** Выходы хода, названные законом. Ищутся по началу строки таблицы, а не по всему тексту. */
73
+ const EXITS = ['вопрос владельцу', 'отказ гарда', 'заполненное окно', 'работа отдана'];
74
+
75
+ function main() {
76
+ if (!existsSync(MAP)) {
77
+ console.log('check-turn-map: карты хода в дереве нет — сверять нечего');
78
+
79
+ return 0;
80
+ }
81
+
82
+ const text = readFileSync(MAP, 'utf8');
83
+ const bytes = statSync(MAP).size;
84
+ const faults = [];
85
+
86
+ if (bytes > LIMIT_BYTES) {
87
+ faults.push(`карта выросла: ${bytes} байт при пределе ${LIMIT_BYTES}`);
88
+ }
89
+
90
+ const inMap = statesOf(text);
91
+
92
+ if (inMap.length === 0) {
93
+ faults.push('в карте нет ни одного состояния — таблица сломана');
94
+ }
95
+
96
+ for (const state of inMap) {
97
+ if (!state.rest[0]) {
98
+ faults.push(`${state.name}: в карте нет обязательного действия`);
99
+ }
100
+ }
101
+
102
+ if (existsSync(RULE)) {
103
+ const inRule = statesOf(readFileSync(RULE, 'utf8'));
104
+ const mapNames = new Set(inMap.map((state) => state.name));
105
+ const ruleNames = new Set(inRule.map((state) => state.name));
106
+
107
+ for (const state of ruleNames) {
108
+ if (!mapNames.has(state)) {
109
+ faults.push(`${state}: состояние объявлено правилом и забыто в карте`);
110
+ }
111
+ }
112
+
113
+ for (const state of mapNames) {
114
+ if (!ruleNames.has(state)) {
115
+ faults.push(`${state}: состояние стоит в карте, а правило его не объявляет`);
116
+ }
117
+ }
118
+ }
119
+
120
+ for (const exit of EXITS) {
121
+ if (!text.includes(exit)) {
122
+ faults.push(`выход хода «${exit}» в карте не назван`);
123
+ }
124
+ }
125
+
126
+ if (faults.length > 0) {
127
+ console.log(`check-turn-map: расхождений ${faults.length}, размер ${bytes} байт при пределе ${LIMIT_BYTES}\n`);
128
+
129
+ for (const fault of faults) {
130
+ console.log(` ${fault}`);
131
+ }
132
+
133
+ console.log('\nКарта короче правила — этим она и полезна. Выросшая, она съедает то окно, ради');
134
+ console.log('которого её кладут в контекст. Правится она в ресурсе пакета, а не в разложенной копии.');
135
+
136
+ return 1;
137
+ }
138
+
139
+ console.log(
140
+ `check-turn-map: ${inMap.length} состояний, ${EXITS.length} выхода хода, ` + `${bytes} байт при пределе ${LIMIT_BYTES} — сошлось`
141
+ );
142
+
143
+ return 0;
144
+ }
145
+
146
+ process.exit(main());
@@ -67,10 +67,24 @@ const DEFAULTS = {
67
67
  */
68
68
  generatedDirs: [],
69
69
  /**
70
- * Предел длины файла одно число на все роды: своё число каждому роду означает спор о
71
- * числе на каждой правке, а не о длине файла.
70
+ * Предел длины файла для кода и обвязки. Родов у него по-прежнему не два десятка, а два:
71
+ * код и текст слоя правил. Спорить о числе на каждой правке нечему оба числа стоят
72
+ * здесь и меняются работой, а не по ходу.
72
73
  */
73
74
  fileSizeLimit: 500,
75
+ /**
76
+ * Предел длины текста слоя правил — закона, правила, паттерна, холодной части и их
77
+ * источников. Он ниже предела кода вдвое, и это не строгость ради строгости: текст, не
78
+ * влезающий на экран целиком, дописывают в конец, не перечитав начала, — так в одном файле
79
+ * и оказываются два ответа на один вопрос. У кода от этого спасает линтер, у текста —
80
+ * только это число.
81
+ */
82
+ proseSizeLimit: 300,
83
+ /**
84
+ * Корни, под которыми лежит текст слоя правил, и его источники. Файл отсюда судится
85
+ * пределом текста, всё остальное — пределом кода. Пусто — предел один на всё дерево.
86
+ */
87
+ proseRoots: [],
74
88
  /**
75
89
  * Внешние пакеты, чьи перечисления считаются наравне с либами: своё перечисление под уже
76
90
  * объявленный там набор — такая же копия, как и между двумя либами. Каждая запись — имя