@rt-tools/agent-kit 0.9.1 → 0.11.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 (119) hide show
  1. package/README.md +5 -0
  2. package/assets/agents/conscience.md +58 -0
  3. package/assets/agents/prose-editor.md +44 -0
  4. package/assets/agents/strict-teacher.md +59 -0
  5. package/assets/checks/board.github.mjs +56 -1
  6. package/assets/checks/check-board.github.mjs +24 -0
  7. package/assets/checks/check-doc-paths.mjs +4 -15
  8. package/assets/checks/check-dupes.mjs +5 -6
  9. package/assets/checks/check-file-size.mjs +6 -20
  10. package/assets/checks/check-prose-style.mjs +137 -0
  11. package/assets/checks/check-reuse.mjs +5 -5
  12. package/assets/checks/check-state-next.mjs +194 -0
  13. package/assets/checks/check-states.mjs +142 -0
  14. package/assets/checks/check-styles.mjs +5 -5
  15. package/assets/checks/check-turn-map.mjs +146 -0
  16. package/assets/checks/lib-common.mjs +3 -3
  17. package/assets/checks/rt-kit-checks.config.mjs +85 -1
  18. package/assets/commands/agent-kit-digest.md +6 -5
  19. package/assets/defaults/project.sh +108 -6
  20. package/assets/defaults/turn-map.md +46 -0
  21. package/assets/docs/GLOSSARY.md +17 -15
  22. package/assets/hooks/browser-device-id.sh +2 -0
  23. package/assets/hooks/browser-guard-device-id.sh +11 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +11 -1
  25. package/assets/hooks/browser-guard-no-listing.sh +11 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +9 -1
  27. package/assets/hooks/browser-guard-require-select.sh +13 -2
  28. package/assets/hooks/claim-guard.sh +115 -0
  29. package/assets/hooks/commit-msg.sh +2 -0
  30. package/assets/hooks/conscience-guard.sh +100 -0
  31. package/assets/hooks/constitution-index.sh +2 -0
  32. package/assets/hooks/deny-tail.sh +32 -0
  33. package/assets/hooks/dev-server-guard.sh +10 -2
  34. package/assets/hooks/docs-guard.sh +16 -2
  35. package/assets/hooks/exam-guard.sh +123 -0
  36. package/assets/hooks/git-guard-delivery-signature.sh +77 -0
  37. package/assets/hooks/git-guard-delivery.sh +156 -68
  38. package/assets/hooks/git-guard-main.sh +14 -0
  39. package/assets/hooks/git-guard-push-tests.sh +51 -2
  40. package/assets/hooks/glossary-load.sh +2 -0
  41. package/assets/hooks/grill-gate.sh +14 -0
  42. package/assets/hooks/handoff-entry-guard.sh +73 -0
  43. package/assets/hooks/handoff-write.sh +103 -0
  44. package/assets/hooks/lint-after-edit.sh +2 -0
  45. package/assets/hooks/observe.sh +2 -0
  46. package/assets/hooks/postmortem-guard.sh +14 -0
  47. package/assets/hooks/proposal-guard.sh +14 -0
  48. package/assets/hooks/prose-style-guard.sh +75 -0
  49. package/assets/hooks/qa-dataid-guard.sh +14 -1
  50. package/assets/hooks/rerun-guard.sh +88 -0
  51. package/assets/hooks/reuse-first-guard.sh +14 -1
  52. package/assets/hooks/roles.sh +34 -0
  53. package/assets/hooks/skill-gate-layers.sh +2 -0
  54. package/assets/hooks/skill-gate-rearm.sh +2 -0
  55. package/assets/hooks/skill-gate.sh +14 -0
  56. package/assets/hooks/skill-loaded.sh +2 -0
  57. package/assets/hooks/sql-guard-parse.sh +2 -0
  58. package/assets/hooks/sql-guard-request.sh +2 -0
  59. package/assets/hooks/sql-guard-target.sh +2 -0
  60. package/assets/hooks/sql-guard-write.sh +4 -1
  61. package/assets/hooks/sql-guard.sh +14 -1
  62. package/assets/hooks/task-context-load.sh +2 -0
  63. package/assets/hooks/task-flow-guard.sh +73 -7
  64. package/assets/hooks/turn-entry-load.sh +62 -0
  65. package/assets/hooks/turn-exit-guard.sh +191 -0
  66. package/assets/hooks/utf8.sh +35 -0
  67. package/assets/hooks/waiting-turn-guard.sh +14 -0
  68. package/assets/hooks/window-fill-guard.sh +43 -2
  69. package/assets/laws/work-conduct.md +29 -0
  70. package/assets/patterns/cargo-triage-mark.md +119 -0
  71. package/assets/patterns/task-flow-close.md +29 -8
  72. package/assets/patterns/task-flow-handoff.md +19 -1
  73. package/assets/patterns/task-flow-resume.md +36 -4
  74. package/assets/patterns/task-flow-start.md +56 -6
  75. package/assets/patterns/turn-entry-map.md +81 -0
  76. package/assets/rules/cargo-triage.md +126 -0
  77. package/assets/rules/git-workflow.azure.md +36 -9
  78. package/assets/rules/git-workflow.github.md +74 -9
  79. package/assets/rules/git-workflow.gitlab.md +40 -12
  80. package/assets/rules/task-flow.md +136 -71
  81. package/assets/rules/testing.md +15 -1
  82. package/assets/rules/turn-entry.md +93 -0
  83. package/assets/samples/tasks/_template/plan.md +4 -1
  84. package/assets/samples/tasks/_template/progress.md +1 -0
  85. package/assets/skills/agent-kit.md +33 -0
  86. package/assets/templates/project.sh +16 -0
  87. package/bin/agent-kit.d.ts.map +1 -1
  88. package/bin/agent-kit.js +42 -1
  89. package/bin/agent-kit.js.map +1 -1
  90. package/lib/cargo-state.d.ts +62 -0
  91. package/lib/cargo-state.d.ts.map +1 -0
  92. package/lib/cargo-state.js +118 -0
  93. package/lib/cargo-state.js.map +1 -0
  94. package/lib/cargo.d.ts +42 -0
  95. package/lib/cargo.d.ts.map +1 -1
  96. package/lib/cargo.js +2 -0
  97. package/lib/cargo.js.map +1 -1
  98. package/lib/commands.d.ts.map +1 -1
  99. package/lib/commands.js +64 -4
  100. package/lib/commands.js.map +1 -1
  101. package/lib/config.d.ts +8 -0
  102. package/lib/config.d.ts.map +1 -1
  103. package/lib/config.js +1 -0
  104. package/lib/config.js.map +1 -1
  105. package/lib/observations.d.ts +35 -1
  106. package/lib/observations.d.ts.map +1 -1
  107. package/lib/observations.js +14 -2
  108. package/lib/observations.js.map +1 -1
  109. package/lib/ship.d.ts +3 -0
  110. package/lib/ship.d.ts.map +1 -1
  111. package/lib/ship.js +56 -0
  112. package/lib/ship.js.map +1 -1
  113. package/lib/thresholds.d.ts +49 -0
  114. package/lib/thresholds.d.ts.map +1 -0
  115. package/lib/thresholds.js +151 -0
  116. package/lib/thresholds.js.map +1 -0
  117. package/package.json +1 -1
  118. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
  119. package/rt-tools-agent-kit-0.9.1.tgz +0 -0
@@ -0,0 +1,194 @@
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
+ const SKILLS = join(ROOT, '.claude/skills');
35
+ const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
36
+ const LAW = join(ROOT, 'docs/constitution/work-conduct.md');
37
+
38
+ /** Зачин строки: по нему её находят, а хвост у каждого раздела свой. */
39
+ const MARKER = '**Следующее движение:**';
40
+
41
+ /** Утверждение о границе состояния. Стоит в правиле, в карте и в законе теми же словами. */
42
+ const BOUNDARY = 'Переход из состояния в состояние';
43
+
44
+ /** Короче этого хвост движения не называет: зачин без движения — та же пустота. */
45
+ const MIN_TAIL = 20;
46
+
47
+ /** Имя состояния и паттерн, который его ведёт, — из таблицы правила. */
48
+ function statesFromRule(text) {
49
+ const states = [];
50
+
51
+ for (const line of text.split('\n')) {
52
+ if (!line.startsWith('|')) {
53
+ continue;
54
+ }
55
+
56
+ const cells = line
57
+ .split('|')
58
+ .slice(1, -1)
59
+ .map((cell) => cell.trim());
60
+
61
+ if (cells.length < 4) {
62
+ continue;
63
+ }
64
+
65
+ const name = cells[0].match(/^`([^`]+)`$/);
66
+ const pattern = cells[3].match(/^`([^`]+)`$/);
67
+
68
+ if (name && pattern) {
69
+ states.push({ name: name[1], pattern: pattern[1] });
70
+ }
71
+ }
72
+
73
+ return states;
74
+ }
75
+
76
+ /** Разделы состояний паттерна: имя, заголовок целиком и найденные в разделе строки движения. */
77
+ function sectionsOf(pattern) {
78
+ const file = join(SKILLS, pattern, 'SKILL.md');
79
+
80
+ if (!existsSync(file)) {
81
+ return null;
82
+ }
83
+
84
+ const sections = [];
85
+ let current = null;
86
+
87
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
88
+ const heading = line.match(/^#+\s+Состояние\s+`([^`]+)`/);
89
+
90
+ if (heading) {
91
+ current = { name: heading[1], heading: line.replace(/^#+\s+/, ''), moves: [] };
92
+ sections.push(current);
93
+ continue;
94
+ }
95
+
96
+ if (current && line.startsWith(MARKER)) {
97
+ current.moves.push(line.slice(MARKER.length).trim());
98
+ }
99
+ }
100
+
101
+ return sections;
102
+ }
103
+
104
+ if (!existsSync(RULE)) {
105
+ console.log('check-state-next: правила ведения работы в дереве нет — сверять нечего');
106
+ process.exit(0);
107
+ }
108
+
109
+ const ruleText = readFileSync(RULE, 'utf8');
110
+ const states = statesFromRule(ruleText);
111
+
112
+ if (states.length === 0) {
113
+ console.error('check-state-next: в правиле ведения работы не нашлось таблицы состояний');
114
+ process.exit(1);
115
+ }
116
+
117
+ const problems = [];
118
+ const patterns = new Map();
119
+ const tails = new Map();
120
+ let counted = 0;
121
+
122
+ for (const state of states) {
123
+ if (!patterns.has(state.pattern)) {
124
+ patterns.set(state.pattern, sectionsOf(state.pattern));
125
+ }
126
+ }
127
+
128
+ for (const [pattern, sections] of patterns) {
129
+ if (sections === null) {
130
+ continue;
131
+ }
132
+
133
+ for (const section of sections) {
134
+ counted += 1;
135
+
136
+ const where = `\`${section.name}\` в паттерне \`${pattern}\``;
137
+
138
+ if (section.moves.length === 0) {
139
+ problems.push(`${where}: в разделе «${section.heading}» нет строки «${MARKER}»`);
140
+ continue;
141
+ }
142
+
143
+ if (section.moves.length > 1) {
144
+ problems.push(`${where}: в разделе «${section.heading}» таких строк ${section.moves.length}, а движение одно`);
145
+ continue;
146
+ }
147
+
148
+ const tail = section.moves[0];
149
+
150
+ if (tail.length < MIN_TAIL) {
151
+ problems.push(`${where}: зачин есть, а движение за ним не названо`);
152
+ continue;
153
+ }
154
+
155
+ const twin = tails.get(tail);
156
+
157
+ if (twin) {
158
+ problems.push(`${where}: движение слово в слово то же, что у ${twin}`);
159
+ continue;
160
+ }
161
+
162
+ tails.set(tail, `\`${section.name}\` в паттерне \`${pattern}\``);
163
+ }
164
+ }
165
+
166
+ if (counted === 0) {
167
+ problems.push('ни одного раздела состояния не нашлось: паттерны не разложены или заголовки в них другие');
168
+ }
169
+
170
+ if (!ruleText.includes(BOUNDARY)) {
171
+ problems.push(`правило ведения работы о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
172
+ }
173
+
174
+ for (const [file, what] of [
175
+ [MAP, 'карта хода'],
176
+ [LAW, 'закон о ведении работы'],
177
+ ]) {
178
+ if (existsSync(file) && !readFileSync(file, 'utf8').includes(BOUNDARY)) {
179
+ problems.push(`${what} о границе состояния молчит: строки «${BOUNDARY}» в ней нет`);
180
+ }
181
+ }
182
+
183
+ if (problems.length > 0) {
184
+ console.error(`check-state-next: расхождений ${problems.length}`);
185
+
186
+ for (const problem of problems) {
187
+ console.error(` ${problem}`);
188
+ }
189
+
190
+ console.error('\nСтрока следующего движения стоит в каждом разделе состояния: зачин общий, движение своё.');
191
+ process.exit(1);
192
+ }
193
+
194
+ 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}, у каждого есть раздел в ведущем паттерне`);
@@ -33,7 +33,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
33
33
  import { createRequire } from 'node:module';
34
34
  import { basename, dirname, join } from 'node:path';
35
35
 
36
- import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
36
+ import { allowlistOf, baselineOf, CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
37
37
 
38
38
  const ALLOWLIST = allowlistOf('styles');
39
39
  const SOURCE_ROOTS = CONFIG.sourceRoots;
@@ -51,9 +51,9 @@ const USE_RE = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
51
51
  /** Строка списка известного: имя класса и перечень файлов при нём */
52
52
  const KEY_RE = /^elem (\S+) @ (.*)$/;
53
53
 
54
- const allowlist = JSON.parse(readFileSync(join(ROOT, ALLOWLIST), 'utf8'));
55
- const known = new Set([...(allowlist.accepted ?? []), ...(allowlist.debt ?? [])]);
56
- const debt = new Set(allowlist.debt ?? []);
54
+ const allowlist = parseAllowlist('styles');
55
+ const known = allowlist.keys;
56
+ const debt = new Set(allowlist.debt.keys());
57
57
 
58
58
  /** Строки списка по имени класса: перечень файлов в ключ входит, но сверяется отдельно */
59
59
  const knownByName = new Map();
@@ -223,7 +223,7 @@ for (const [name, files] of [...usedIn].sort(([first], [second]) => first.locale
223
223
  }
224
224
 
225
225
  if (process.argv.includes('--baseline')) {
226
- console.log(JSON.stringify({ ...allowlist, debt: findings.map((finding) => finding.key).sort() }, null, 4));
226
+ console.log(baselineOf(findings.map((finding) => finding.key).sort(), allowlist));
227
227
  process.exit(0);
228
228
  }
229
229
 
@@ -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());
@@ -9,7 +9,7 @@
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
 
12
- import { CONFIG, ROOT, readAllowlist } from './rt-kit-checks.config.mjs';
12
+ import { CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
13
13
 
14
14
  const FAMILIES = CONFIG.families;
15
15
  /**
@@ -60,8 +60,8 @@ const isLib = (path) => existsSync(join(ROOT, path, 'project.json'));
60
60
  * нет вовсе, и прямое чтение роняло проверку отказом «нет такого файла» — то есть первая же
61
61
  * установка получала поломку вместо отчёта о том, что долгов нет.
62
62
  */
63
- const allowlist = readAllowlist('lib-layers');
64
- const pathsOf = (key) => (allowlist[key] ?? []).map((exception) => exception.path);
63
+ const allowlist = parseAllowlist('lib-layers', ['notDomains', 'legacyDomains', 'legacyLibs', 'singleLayerDomains', 'accepted', 'debt']);
64
+ const pathsOf = (key) => [...allowlist[key].keys()];
65
65
 
66
66
  /** Паттерн `<корень>/x/*` покрывает и сам каталог `<корень>/x`: исключение снимается целиком */
67
67
  const matches = (patterns, path) =>
@@ -187,8 +187,92 @@ export const allowlistOf = (name) => join(CONFIG.allowlistDir, `${name}-allowlis
187
187
  */
188
188
  export const readAllowlist = (name) => {
189
189
  const path = join(ROOT, allowlistOf(name));
190
+ if (!existsSync(path)) {
191
+ return {};
192
+ }
193
+ try {
194
+ return JSON.parse(readFileSync(path, 'utf8'));
195
+ } catch (error) {
196
+ // Нечитаемая настройка — это не пустой список, и молчать о ней нельзя: проверка,
197
+ // прочитавшая пустоту вместо перечня, назовёт долгом всё дерево разом.
198
+ console.error(`${allowlistOf(name)}: список известного не прочитан — не разбирается как JSON: ${error.message}`);
199
+ process.exit(1);
200
+ }
201
+ };
202
+
203
+ /**
204
+ * Разбор списка принятого: запись отвечает за себя сама.
205
+ *
206
+ * Причина, написанная прозой на весь список, за отдельную строку не отвечает: список пустеет и
207
+ * наполняется, а причина остаётся прежней — строка, внесённая позже, выглядит покрытой ею. Так
208
+ * и вышло у четырёх списков дерева: шапка говорила о разобранном долге, а под ней лежало
209
+ * принятое, которого в тот день ещё не было.
210
+ *
211
+ * Поэтому форма одна на все списки: сторона — объект, где ключ говорит, что принято, а
212
+ * значение несёт причину и номер задачи, которой запись внесена. Стороны называет зовущий:
213
+ * у большинства списков это `accepted` и `debt`, у иных свои имена, а разбор у всех один. Номер — это дорога
214
+ * к разговору, в котором заглушить разрешили: без него запись объясняет сама себя, а спросить
215
+ * о ней некого.
216
+ *
217
+ * Отказ называет файл и саму запись: список читают не целиком, а по строке, и «где-то здесь
218
+ * неверная запись» стоит того же, что и молчание.
219
+ */
220
+ export const parseAllowlist = (name, sides = ['accepted', 'debt']) => {
221
+ const file = allowlistOf(name);
222
+ const raw = readAllowlist(name);
223
+ const key = CONFIG.board.taskKey;
224
+ const taskForm = key ? new RegExp(`^${key}-\\d+$`) : /^[A-Za-z]+-\d+$/;
225
+ const refuse = (message) => {
226
+ console.error(`${file}: ${message}`);
227
+ process.exit(1);
228
+ };
229
+ const parseSide = (side) => {
230
+ const entries = raw[side];
231
+ if (entries === undefined) {
232
+ return new Map();
233
+ }
234
+ if (Array.isArray(entries) || typeof entries !== 'object' || entries === null) {
235
+ refuse(`«${side}» записан не объектом — у записи нет места ни для причины, ни для номера задачи`);
236
+ }
237
+ const parsed = new Map();
238
+ for (const [entry, value] of Object.entries(entries)) {
239
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
240
+ refuse(`«${entry}» записан без причины — рядом с записью стоят «reason» и «task»`);
241
+ }
242
+ if (typeof value.reason !== 'string' || value.reason.trim() === '') {
243
+ refuse(`у «${entry}» пустая причина — заглушённое без причины через месяц не отличить от забытого`);
244
+ }
245
+ if (typeof value.task !== 'string' || !taskForm.test(value.task)) {
246
+ refuse(`у «${entry}» нет номера задачи вида «${key || 'КЛЮЧ'}-<номер>» — спросить о записи будет некого`);
247
+ }
248
+ parsed.set(entry, { reason: value.reason, task: value.task });
249
+ }
250
+
251
+ return parsed;
252
+ };
253
+ const parsed = Object.fromEntries(sides.map((side) => [side, parseSide(side)]));
254
+
255
+ return { ...parsed, keys: new Set(sides.flatMap((side) => [...parsed[side].keys()])) };
256
+ };
257
+
258
+ /**
259
+ * Заготовка списка принятого для режима пересъёмки: прежние записи сохраняются целиком, а
260
+ * новые приходят с пустой причиной и пустым номером задачи.
261
+ *
262
+ * Пустые поля здесь намеренны. Пересъёмка — это помощник, а не разрешение: заглушить проверку
263
+ * можно только словом владельца, и записать его должен человек. Разбор такую запись отбивает,
264
+ * поэтому список, снятый пересъёмкой и не заполненный, дальше гейта не проходит.
265
+ */
266
+ export const baselineOf = (keys, parsed, side = 'debt') => {
267
+ const entryOf = (key) => parsed.debt?.get(key) ?? parsed.accepted?.get(key) ?? { reason: '', task: '' };
268
+ const fresh = keys.filter((key) => !parsed.keys.has(key));
269
+ if (fresh.length > 0) {
270
+ console.error(`новых записей ${fresh.length} — у каждой заполняются «reason» и «task», иначе разбор списка отбивает прогон`);
271
+ }
272
+ const filled = Object.fromEntries(keys.map((key) => [key, entryOf(key)]));
273
+ const rest = Object.fromEntries([...(parsed.accepted ?? new Map())].filter(([key]) => !keys.includes(key)));
190
274
 
191
- return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
275
+ return JSON.stringify(side === 'accepted' ? { accepted: filled } : { accepted: rest, debt: filled }, null, 4);
192
276
  };
193
277
 
194
278
  /** Есть ли в дереве то, без чего проверке нечего делать. Нет — она выходит с нулём и говорит это. */
@@ -12,9 +12,9 @@ argument-hint: '[пусто | --days N]'
12
12
 
13
13
  ## 1. Собери, что пришло
14
14
 
15
- Груз уезжает в приём, а не в очередь работ: сводка говорит о рабочих привычках команды, и в
16
- открытой очереди это выложено всему свету. Записи приёма читает его админка; пока её нет,
17
- собранное читается запросом к его хранилищу — как именно, сказано в компаньоне этой команды.
15
+ Чем груз читается и что значит взять его в работу, говорит правило `cargo-triage`: здесь этот
16
+ порядок не повторяется, а зовётся. Сведению от него нужен один шаг список неразобранного,
17
+ суженный отбором по состоянию «новое».
18
18
 
19
19
  Записи, заведённые прежним порядком, лежат в очереди работ и никуда не делись:
20
20
 
@@ -66,13 +66,14 @@ npx agent-kit stats --days <отрезок> --json
66
66
  По каждой — ресурс, место, готовый текст и число записей, из которых она вышла. Не правь ничего
67
67
  сам: решение принимает владелец, а работа идёт обычным ходом — задача, ветка, папка задачи.
68
68
 
69
- Заведи задачу на то, что владелец принял:
69
+ Заведи задачу на то, что владелец принял, и тем же ходом отметь вошедшие в неё записи груза —
70
+ как именно, говорит паттерн `cargo-triage-mark`:
70
71
 
71
72
  ```bash
72
73
  npm run task:new -- --title '<что не так>' --slug <короткое-имя> --label enhancement < тело.md
73
74
  ```
74
75
 
75
- Записи, вошедшие в задачу, закрой ссылкой на неё:
76
+ Записи, заведённые в очереди работ прежним порядком, закрываются там же ссылкой на задачу:
76
77
 
77
78
  ```bash
78
79
  gh issue close <номер> --comment 'Вошло в #<номер задачи>.'