@spec-box/sdd 0.6.4

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 (161) hide show
  1. package/README.md +81 -0
  2. package/assets/adapters/openspec/instructions.md +47 -0
  3. package/assets/adapters/spec-box/instructions.md +44 -0
  4. package/assets/ci/Dockerfile +9 -0
  5. package/assets/ci/github-sbox-run.yml +85 -0
  6. package/assets/ci/github-tests.yml +25 -0
  7. package/assets/project/architecture.md +29 -0
  8. package/assets/project/contracts.md +29 -0
  9. package/assets/project/conventions.md +29 -0
  10. package/assets/project/decisions.README.md +3 -0
  11. package/assets/project/glossary.md +13 -0
  12. package/assets/project/overview.md +29 -0
  13. package/assets/project/testing.md +34 -0
  14. package/assets/project/workflow.md +29 -0
  15. package/assets/prompts/fill-project-docs.md +43 -0
  16. package/assets/roles/challenger.md +33 -0
  17. package/assets/roles/distiller.md +18 -0
  18. package/assets/roles/implementer.md +39 -0
  19. package/assets/roles/planner.md +58 -0
  20. package/assets/roles/researcher.md +47 -0
  21. package/assets/roles/reviewer.md +68 -0
  22. package/assets/roles/tester.md +36 -0
  23. package/assets/roles/verifier.md +57 -0
  24. package/assets/schema/default.yaml +121 -0
  25. package/assets/schema/sbox-answer.schema.json +42 -0
  26. package/assets/templates/coverage.yaml +12 -0
  27. package/assets/templates/design.md +48 -0
  28. package/assets/templates/proposal.md +27 -0
  29. package/assets/templates/tasks.md +9 -0
  30. package/assets/templates/test-plan.md +7 -0
  31. package/bin/sbox.js +5 -0
  32. package/dist/adapters/host/claude/index.js +112 -0
  33. package/dist/adapters/host/claude/index.js.map +1 -0
  34. package/dist/adapters/repo/git.js +81 -0
  35. package/dist/adapters/repo/git.js.map +1 -0
  36. package/dist/adapters/repo/github/index.js +136 -0
  37. package/dist/adapters/repo/github/index.js.map +1 -0
  38. package/dist/adapters/repo/index.js +4 -0
  39. package/dist/adapters/repo/index.js.map +1 -0
  40. package/dist/adapters/repo/local.js +47 -0
  41. package/dist/adapters/repo/local.js.map +1 -0
  42. package/dist/adapters/runner/claude.js +129 -0
  43. package/dist/adapters/runner/claude.js.map +1 -0
  44. package/dist/adapters/runner/codex.js +151 -0
  45. package/dist/adapters/runner/codex.js.map +1 -0
  46. package/dist/adapters/runner/index.js +4 -0
  47. package/dist/adapters/runner/index.js.map +1 -0
  48. package/dist/adapters/spec/index.js +4 -0
  49. package/dist/adapters/spec/index.js.map +1 -0
  50. package/dist/adapters/spec/openspec/delta.js +268 -0
  51. package/dist/adapters/spec/openspec/delta.js.map +1 -0
  52. package/dist/adapters/spec/openspec/index.js +107 -0
  53. package/dist/adapters/spec/openspec/index.js.map +1 -0
  54. package/dist/adapters/spec/openspec/parser.js +188 -0
  55. package/dist/adapters/spec/openspec/parser.js.map +1 -0
  56. package/dist/adapters/spec/spec-box/delta.js +198 -0
  57. package/dist/adapters/spec/spec-box/delta.js.map +1 -0
  58. package/dist/adapters/spec/spec-box/index.js +112 -0
  59. package/dist/adapters/spec/spec-box/index.js.map +1 -0
  60. package/dist/adapters/spec/spec-box/yaml.js +74 -0
  61. package/dist/adapters/spec/spec-box/yaml.js.map +1 -0
  62. package/dist/cli/commands/artifacts.js +86 -0
  63. package/dist/cli/commands/artifacts.js.map +1 -0
  64. package/dist/cli/commands/change.js +111 -0
  65. package/dist/cli/commands/change.js.map +1 -0
  66. package/dist/cli/commands/changeset.js +81 -0
  67. package/dist/cli/commands/changeset.js.map +1 -0
  68. package/dist/cli/commands/ci.js +45 -0
  69. package/dist/cli/commands/ci.js.map +1 -0
  70. package/dist/cli/commands/coverage.js +45 -0
  71. package/dist/cli/commands/coverage.js.map +1 -0
  72. package/dist/cli/commands/deliver.js +71 -0
  73. package/dist/cli/commands/deliver.js.map +1 -0
  74. package/dist/cli/commands/doctor.js +55 -0
  75. package/dist/cli/commands/doctor.js.map +1 -0
  76. package/dist/cli/commands/host.js +30 -0
  77. package/dist/cli/commands/host.js.map +1 -0
  78. package/dist/cli/commands/init.js +150 -0
  79. package/dist/cli/commands/init.js.map +1 -0
  80. package/dist/cli/commands/metrics.js +62 -0
  81. package/dist/cli/commands/metrics.js.map +1 -0
  82. package/dist/cli/commands/prompt.js +68 -0
  83. package/dist/cli/commands/prompt.js.map +1 -0
  84. package/dist/cli/commands/protocol.js +151 -0
  85. package/dist/cli/commands/protocol.js.map +1 -0
  86. package/dist/cli/commands/run.js +157 -0
  87. package/dist/cli/commands/run.js.map +1 -0
  88. package/dist/cli/commands/spec.js +84 -0
  89. package/dist/cli/commands/spec.js.map +1 -0
  90. package/dist/cli/commands/wiring.js +50 -0
  91. package/dist/cli/commands/wiring.js.map +1 -0
  92. package/dist/cli/context.js +18 -0
  93. package/dist/cli/context.js.map +1 -0
  94. package/dist/cli/main.js +63 -0
  95. package/dist/cli/main.js.map +1 -0
  96. package/dist/cli/output.js +31 -0
  97. package/dist/cli/output.js.map +1 -0
  98. package/dist/core/archive.js +77 -0
  99. package/dist/core/archive.js.map +1 -0
  100. package/dist/core/change.js +240 -0
  101. package/dist/core/change.js.map +1 -0
  102. package/dist/core/changeset.js +98 -0
  103. package/dist/core/changeset.js.map +1 -0
  104. package/dist/core/config.js +144 -0
  105. package/dist/core/config.js.map +1 -0
  106. package/dist/core/coverage.js +69 -0
  107. package/dist/core/coverage.js.map +1 -0
  108. package/dist/core/deliver.js +190 -0
  109. package/dist/core/deliver.js.map +1 -0
  110. package/dist/core/diagnostics.js +12 -0
  111. package/dist/core/diagnostics.js.map +1 -0
  112. package/dist/core/drift.js +44 -0
  113. package/dist/core/drift.js.map +1 -0
  114. package/dist/core/errors.js +12 -0
  115. package/dist/core/errors.js.map +1 -0
  116. package/dist/core/gates.js +76 -0
  117. package/dist/core/gates.js.map +1 -0
  118. package/dist/core/lock.js +57 -0
  119. package/dist/core/lock.js.map +1 -0
  120. package/dist/core/metrics.js +140 -0
  121. package/dist/core/metrics.js.map +1 -0
  122. package/dist/core/packet.js +172 -0
  123. package/dist/core/packet.js.map +1 -0
  124. package/dist/core/paths.js +63 -0
  125. package/dist/core/paths.js.map +1 -0
  126. package/dist/core/phases.js +203 -0
  127. package/dist/core/phases.js.map +1 -0
  128. package/dist/core/pr-body.js +64 -0
  129. package/dist/core/pr-body.js.map +1 -0
  130. package/dist/core/project-docs.js +246 -0
  131. package/dist/core/project-docs.js.map +1 -0
  132. package/dist/core/protect.js +47 -0
  133. package/dist/core/protect.js.map +1 -0
  134. package/dist/core/repo-host.js +11 -0
  135. package/dist/core/repo-host.js.map +1 -0
  136. package/dist/core/report.js +286 -0
  137. package/dist/core/report.js.map +1 -0
  138. package/dist/core/result.js +90 -0
  139. package/dist/core/result.js.map +1 -0
  140. package/dist/core/roles.js +18 -0
  141. package/dist/core/roles.js.map +1 -0
  142. package/dist/core/run.js +222 -0
  143. package/dist/core/run.js.map +1 -0
  144. package/dist/core/runner.js +40 -0
  145. package/dist/core/runner.js.map +1 -0
  146. package/dist/core/schema.js +104 -0
  147. package/dist/core/schema.js.map +1 -0
  148. package/dist/core/spec-adapter.js +12 -0
  149. package/dist/core/spec-adapter.js.map +1 -0
  150. package/dist/core/spec-model.js +8 -0
  151. package/dist/core/spec-model.js.map +1 -0
  152. package/dist/core/tasks.js +15 -0
  153. package/dist/core/tasks.js.map +1 -0
  154. package/dist/core/test-reports.js +60 -0
  155. package/dist/core/test-reports.js.map +1 -0
  156. package/dist/core/wiki.js +86 -0
  157. package/dist/core/wiki.js.map +1 -0
  158. package/dist/core/wiring.js +65 -0
  159. package/dist/core/wiring.js.map +1 -0
  160. package/docs/design.md +829 -0
  161. package/package.json +66 -0
@@ -0,0 +1,246 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { diag } from './diagnostics.js';
5
+ import { exists, readText, toPosix } from './paths.js';
6
+ export const DOC_CATEGORIES = [
7
+ {
8
+ id: 'product',
9
+ file: 'overview.md',
10
+ title: 'Продукт и границы',
11
+ questions: 'Что за продукт, кто пользователи, какие системы рядом, где живёт код, где истина спецификаций',
12
+ sections: ['Что за продукт', 'Пользователи', 'Системы рядом', 'Где живёт код', 'Истина спецификаций'],
13
+ readers: ['researcher', 'planner', 'challenger'],
14
+ required: true,
15
+ },
16
+ {
17
+ id: 'architecture',
18
+ file: 'architecture.md',
19
+ title: 'Архитектура и карта кода',
20
+ questions: 'Какие пакеты и модули есть, точки входа, поток данных, где что лежит, границы модулей, генерируемый код',
21
+ sections: ['Карта пакетов и модулей', 'Точки входа', 'Поток данных', 'Границы модулей', 'Генерируемый код'],
22
+ readers: ['researcher', 'planner', 'implementer', 'challenger'],
23
+ required: true,
24
+ },
25
+ {
26
+ id: 'conventions',
27
+ file: 'conventions.md',
28
+ title: 'Соглашения по коду',
29
+ questions: 'Язык кода и комментариев, стиль, запрещённые конструкции, именование, правила коммитов',
30
+ sections: ['Язык', 'Стиль', 'Запрещено', 'Именование', 'Коммиты'],
31
+ readers: ['implementer', 'reviewer', 'tester'],
32
+ required: true,
33
+ },
34
+ {
35
+ id: 'testing',
36
+ file: 'testing.md',
37
+ title: 'Тестирование и проверки',
38
+ questions: 'Уровни тестов, команды узкого и полного прогона, пути отчётов, правило именования тестов по сценариям, среда e2e, что нельзя автоматизировать',
39
+ sections: ['Уровни тестов', 'Команды', 'Отчёты', 'Именование тестов по сценариям', 'Среда e2e', 'Не автоматизируется'],
40
+ readers: ['tester', 'implementer', 'verifier', 'reviewer'],
41
+ required: true,
42
+ },
43
+ {
44
+ id: 'workflow',
45
+ file: 'workflow.md',
46
+ title: 'Рабочий процесс',
47
+ questions: 'Ветки, пул-реквесты, обязательные проверки CI, что запрещено без человека, разрешённые команды для агентов',
48
+ sections: ['Ветки', 'Пул-реквесты', 'Проверки CI', 'Запрещено без человека', 'Разрешённые команды'],
49
+ readers: ['implementer', 'tester', 'verifier'],
50
+ required: true,
51
+ },
52
+ {
53
+ id: 'glossary',
54
+ file: 'glossary.md',
55
+ title: 'Словарь домена',
56
+ questions: 'Термины в формулировках, которыми пишутся спецификации и тесты',
57
+ sections: ['Термины'],
58
+ readers: ['researcher', 'planner', 'tester', 'implementer', 'reviewer', 'verifier', 'challenger'],
59
+ required: true,
60
+ },
61
+ {
62
+ id: 'contracts',
63
+ file: 'contracts.md',
64
+ title: 'Интеграции и контракты',
65
+ questions: 'Внешние API, события, схемы, кто потребитель, где лежат машиночитаемые контракты, репозитории-партнёры',
66
+ sections: ['Внешние API', 'События', 'Схемы', 'Потребители', 'Машиночитаемые контракты'],
67
+ readers: ['planner', 'tester', 'challenger'],
68
+ required: false,
69
+ },
70
+ ];
71
+ export const DOC_FRONTMATTER_FIELDS = ['id', 'summary', 'read_when', 'updated', 'verification'];
72
+ const VERIFICATION_VALUES = new Set(['verified', 'needs-review']);
73
+ const PLACEHOLDER = /(\bTODO\b|\bTBD\b|\bFIXME\b|\{\{[^}]+\}\}|<заполн[^>]*>|<fill[^>]*>)/iu;
74
+ export function parseDoc(text) {
75
+ let frontmatter = {};
76
+ let body = text;
77
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
78
+ if (m) {
79
+ try {
80
+ frontmatter = YAML.parse(m[1]) ?? {};
81
+ }
82
+ catch {
83
+ frontmatter = { __invalid: true };
84
+ }
85
+ body = text.slice(m[0].length);
86
+ }
87
+ const headings = [];
88
+ body.split(/\r?\n/).forEach((line, i) => {
89
+ const h = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
90
+ if (h)
91
+ headings.push({ level: h[1].length, text: h[2].trim(), line: i + 1 });
92
+ });
93
+ return { frontmatter, body, headings };
94
+ }
95
+ export function docsDir(root, config) {
96
+ return path.join(root, config.project.docs);
97
+ }
98
+ export function docsForRole(role) {
99
+ return DOC_CATEGORIES.filter((c) => c.readers.includes(role));
100
+ }
101
+ const norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
102
+ /** Структурный уровень `sbox doctor`: наличие, фронтматтер, разделы, заглушки, ссылки на пути и команды, правила. */
103
+ export function doctorDocs(root, config) {
104
+ const out = [];
105
+ const dir = docsDir(root, config);
106
+ if (!exists(dir)) {
107
+ out.push(diag('error', 'DOCS_DIR_MISSING', `Нет каталога проектной документации ${toPosix(path.relative(root, dir))}`, undefined, 'Выполните `sbox init`.'));
108
+ return out;
109
+ }
110
+ const scripts = readPackageScripts(root);
111
+ for (const category of DOC_CATEGORIES) {
112
+ const file = path.join(dir, category.file);
113
+ const target = toPosix(path.relative(root, file));
114
+ if (!exists(file)) {
115
+ if (category.required)
116
+ out.push(diag('error', 'DOC_MISSING', `Нет файла категории «${category.title}»`, target, `Создайте ${category.file} с разделами: ${category.sections.join(', ')}.`));
117
+ else
118
+ out.push(diag('info', 'DOC_OPTIONAL_MISSING', `Необязательная категория «${category.title}» не заполнена`, target));
119
+ continue;
120
+ }
121
+ const text = readText(file);
122
+ const doc = parseDoc(text);
123
+ if (doc.frontmatter.__invalid)
124
+ out.push(diag('error', 'DOC_FRONTMATTER_INVALID', 'Фронтматтер не разбирается как YAML', target));
125
+ for (const field of DOC_FRONTMATTER_FIELDS) {
126
+ if (doc.frontmatter[field] === undefined || doc.frontmatter[field] === '') {
127
+ out.push(diag('error', 'DOC_FRONTMATTER_FIELD', `В фронтматтере нет поля ${field}`, target));
128
+ }
129
+ }
130
+ const verification = doc.frontmatter.verification;
131
+ if (typeof verification === 'string' && !VERIFICATION_VALUES.has(verification)) {
132
+ out.push(diag('error', 'DOC_VERIFICATION_VALUE', `verification должно быть verified или needs-review, а не «${verification}»`, target));
133
+ }
134
+ const h2 = doc.headings.filter((h) => h.level === 2).map((h) => norm(h.text));
135
+ for (const section of category.sections) {
136
+ if (!h2.includes(norm(section))) {
137
+ out.push(diag('error', 'DOC_SECTION_MISSING', `Нет раздела «## ${section}»`, target, `Категория «${category.title}» отвечает на: ${category.questions}.`));
138
+ }
139
+ }
140
+ doc.body.split(/\r?\n/).forEach((line, i) => {
141
+ if (PLACEHOLDER.test(line)) {
142
+ out.push(diag('error', 'DOC_PLACEHOLDER', `Заглушка в строке ${i + 1}: ${line.trim().slice(0, 80)}`, target));
143
+ }
144
+ });
145
+ if (wordCount(doc.body) < 40) {
146
+ out.push(diag('warning', 'DOC_TOO_SHORT', 'Содержимое короче 40 слов: категория вряд ли закрыта', target));
147
+ }
148
+ for (const ref of pathRefs(doc.body)) {
149
+ if (!exists(path.join(root, ref))) {
150
+ out.push(diag('warning', 'DOC_PATH_MISSING', `Упомянутый путь не найден в репозитории: ${ref}`, target));
151
+ }
152
+ }
153
+ if (category.id === 'testing' && scripts) {
154
+ for (const cmd of scriptRefs(doc.body)) {
155
+ if (!(cmd in scripts))
156
+ out.push(diag('warning', 'DOC_SCRIPT_MISSING', `Скрипт «${cmd}» упомянут, но его нет в package.json`, target));
157
+ }
158
+ }
159
+ }
160
+ out.push(...doctorDecisions(root, config));
161
+ return out;
162
+ }
163
+ function wordCount(text) {
164
+ return text.split(/\s+/).filter(Boolean).length;
165
+ }
166
+ /** Пути в обратных кавычках: содержат «/», без пробелов, не URL, не глоб. */
167
+ function pathRefs(body) {
168
+ const refs = new Set();
169
+ for (const m of body.matchAll(/`([^`\s]+)`/g)) {
170
+ const v = m[1];
171
+ if (!v.includes('/') || /^[a-z]+:\/\//i.test(v) || /[*?{}[\]]/.test(v) || v.startsWith('/') || v.startsWith('-'))
172
+ continue;
173
+ if (/^[A-Za-z0-9_.@-][A-Za-z0-9_./@-]*$/.test(v))
174
+ refs.add(v.replace(/\/$/, ''));
175
+ }
176
+ return [...refs];
177
+ }
178
+ /** Команды вида `pnpm test:unit`, `npm run lint`, `yarn e2e` → имя скрипта. */
179
+ function scriptRefs(body) {
180
+ const refs = new Set();
181
+ for (const m of body.matchAll(/`(?:pnpm|npm run|yarn)\s+([a-z][\w:-]*)`/g))
182
+ refs.add(m[1]);
183
+ return [...refs];
184
+ }
185
+ function readPackageScripts(root) {
186
+ const file = path.join(root, 'package.json');
187
+ if (!exists(file))
188
+ return null;
189
+ try {
190
+ return JSON.parse(readText(file)).scripts ?? {};
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
196
+ export function decisionsDir(root, config) {
197
+ return path.join(docsDir(root, config), 'decisions');
198
+ }
199
+ export function loadDecisions(root, config) {
200
+ const dir = decisionsDir(root, config);
201
+ if (!exists(dir))
202
+ return [];
203
+ return fs
204
+ .readdirSync(dir)
205
+ .filter((f) => /^ADR-\d+.*\.md$/i.test(f))
206
+ .sort()
207
+ .map((f) => {
208
+ const doc = parseDoc(readText(path.join(dir, f)));
209
+ const fm = doc.frontmatter;
210
+ const ruleSection = doc.body.match(/##\s*Правило\s*\n([\s\S]*?)(?=\n##\s|$)/);
211
+ return {
212
+ id: String(fm.id ?? f),
213
+ title: String(fm.title ?? f),
214
+ status: fm.status ?? 'accepted',
215
+ scope: Array.isArray(fm.scope) ? fm.scope.map(String) : [],
216
+ rule: (ruleSection?.[1] ?? '').trim(),
217
+ file: toPosix(path.relative(root, path.join(dir, f))),
218
+ };
219
+ });
220
+ }
221
+ function doctorDecisions(root, config) {
222
+ const out = [];
223
+ const seen = new Set();
224
+ for (const d of loadDecisions(root, config)) {
225
+ if (!/^ADR-\d{4}$/.test(d.id))
226
+ out.push(diag('error', 'ADR_ID', `Идентификатор правила должен быть вида ADR-0001, а не «${d.id}»`, d.file));
227
+ if (seen.has(d.id))
228
+ out.push(diag('error', 'ADR_DUPLICATE', `Идентификатор ${d.id} повторяется`, d.file));
229
+ seen.add(d.id);
230
+ if (!['accepted', 'superseded'].includes(d.status))
231
+ out.push(diag('error', 'ADR_STATUS', `status должен быть accepted или superseded`, d.file));
232
+ if (!d.rule)
233
+ out.push(diag('error', 'ADR_RULE', 'Нет раздела «## Правило» с формулировкой', d.file));
234
+ if (d.scope.length === 0)
235
+ out.push(diag('warning', 'ADR_SCOPE', 'Не задана область действия scope', d.file));
236
+ }
237
+ return out;
238
+ }
239
+ /** Действующие правила, чья область пересекается с затронутыми путями (или все, если пути неизвестны). */
240
+ export function applicableDecisions(decisions, paths) {
241
+ const accepted = decisions.filter((d) => d.status === 'accepted');
242
+ if (!paths || paths.length === 0)
243
+ return accepted;
244
+ return accepted.filter((d) => d.scope.length === 0 || d.scope.some((s) => paths.some((p) => p.startsWith(s) || s.startsWith(p))));
245
+ }
246
+ //# sourceMappingURL=project-docs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-docs.js","sourceRoot":"","sources":["../../src/core/project-docs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import fg from 'fast-glob';
6
+ const IGNORE = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/coverage/**'];
7
+ function sha256(file) {
8
+ return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
9
+ }
10
+ /** Текущие файлы по шаблонам и их хеши. */
11
+ export function snapshotProtected(root, globs) {
12
+ if (globs.length === 0)
13
+ return {};
14
+ const files = fg.sync(globs, { cwd: root, onlyFiles: true, dot: true, ignore: IGNORE }).sort();
15
+ const out = {};
16
+ for (const f of files)
17
+ out[f] = sha256(path.join(root, f));
18
+ return out;
19
+ }
20
+ /** Расхождения текущей рабочей копии со снимком: изменён, удалён или добавлен файл под защитой. */
21
+ export function protectedViolations(root, globs, snapshot) {
22
+ if (globs.length === 0)
23
+ return [];
24
+ const current = snapshotProtected(root, globs);
25
+ const out = [];
26
+ for (const [p, sha] of Object.entries(snapshot)) {
27
+ if (!(p in current))
28
+ out.push({ path: p, kind: 'deleted' });
29
+ else if (current[p] !== sha)
30
+ out.push({ path: p, kind: 'modified' });
31
+ }
32
+ for (const p of Object.keys(current))
33
+ if (!(p in snapshot))
34
+ out.push({ path: p, kind: 'added' });
35
+ return out.sort((a, b) => a.path.localeCompare(b.path));
36
+ }
37
+ /** Изменённые относительно HEAD отслеживаемые файлы (без untracked): запасной путь для изменений без снимка. */
38
+ export function trackedChangedFiles(root) {
39
+ try {
40
+ const out = execFileSync('git', ['diff', '--name-only', 'HEAD', '--'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
41
+ return out.split('\n').map((l) => l.trim()).filter(Boolean);
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ //# sourceMappingURL=protect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protect.js","sourceRoot":"","sources":["../../src/core/protect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ const registry = new Map();
2
+ export function registerRepoHost(name, factory) {
3
+ registry.set(name, factory);
4
+ }
5
+ export function createRepoHost(root, config, name = config.repo.adapter) {
6
+ const factory = registry.get(name);
7
+ if (!factory)
8
+ throw new Error(`Адаптер репозитория "${name}" не зарегистрирован`);
9
+ return factory(root, config);
10
+ }
11
+ //# sourceMappingURL=repo-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-host.js","sourceRoot":"","sources":["../../src/core/repo-host.ts"],"names":[],"mappings":"AAwDA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,OAAwB;IACrE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,MAAc,EAAE,IAAI,GAAW,MAAM,CAAC,IAAI,CAAC,OAAO;IAC7F,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,sBAAsB,CAAC,CAAC;IAClF,OAAO,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,286 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { diag, hasErrors } from './diagnostics.js';
5
+ import { nextRunId, runDir, saveChange } from './change.js';
6
+ import { isGitRepo, sealChangeSet, writeChangeSet } from './changeset.js';
7
+ import { reconcileDrift } from './drift.js';
8
+ import { completePhase, nextStep, routeBlocker } from './phases.js';
9
+ import { parseRoleResult } from './result.js';
10
+ import { artifactStates, loadWorkflow, pendingArtifacts } from './schema.js';
11
+ import { taskProgress } from './tasks.js';
12
+ import { wiringForChange } from './wiring.js';
13
+ import picomatch from 'picomatch';
14
+ import { protectedViolations, snapshotProtected, trackedChangedFiles } from './protect.js';
15
+ import { exists, readText, toPosix, writeText } from './paths.js';
16
+ export const CHANGESET_EXCLUDE_DEFAULT = ['.sbox/**', 'node_modules/**', 'dist/**', 'build/**', 'coverage/**', 'reports/**'];
17
+ export function changesetExclude(config, changeDirRel) {
18
+ return [...(config.changeset?.exclude ?? CHANGESET_EXCLUDE_DEFAULT), `${changeDirRel}/**`];
19
+ }
20
+ function sha256(text) {
21
+ return createHash('sha256').update(text).digest('hex');
22
+ }
23
+ /** Принять ответ роли: сохранить, разобрать, проверить и перевести изменение в следующее состояние. */
24
+ export async function applyReport(input) {
25
+ const { root, config, dir, change, role, phase, markdown } = input;
26
+ const diagnostics = [];
27
+ const runId = nextRunId(change);
28
+ const rdir = runDir(dir, runId);
29
+ fs.mkdirSync(rdir, { recursive: true });
30
+ const resultFile = path.join(rdir, 'result.md');
31
+ writeText(resultFile, markdown);
32
+ const receiptBase = {
33
+ id: runId,
34
+ role,
35
+ phase,
36
+ attempt: input.receipt?.attempt ?? 1,
37
+ runner: input.receipt?.runner ?? null,
38
+ model: input.receipt?.model ?? null,
39
+ session: input.receipt?.session ?? null,
40
+ started: input.receipt?.started ?? null,
41
+ finished: new Date().toISOString(),
42
+ exit_code: input.receipt?.exitCode ?? null,
43
+ cost_usd: input.receipt?.costUsd ?? null,
44
+ packet_sha256: input.receipt?.packetSha256 ?? (exists(path.join(rdir, 'packet.json')) ? sha256(readText(path.join(rdir, 'packet.json'))) : null),
45
+ prompt_sha256: input.receipt?.promptSha256 ?? null,
46
+ result_sha256: sha256(markdown),
47
+ result_bytes: Buffer.byteLength(markdown),
48
+ events_path: input.receipt?.eventsPath ?? null,
49
+ events_sha256: input.receipt?.eventsSha256 ?? null,
50
+ stderr_sha256: input.receipt?.stderrSha256 ?? null,
51
+ };
52
+ const pushRun = (status, failure) => {
53
+ change.runs.push({
54
+ id: runId,
55
+ role,
56
+ phase,
57
+ attempt: receiptBase.attempt,
58
+ status,
59
+ dir: `runs/${runId}`,
60
+ finished: receiptBase.finished,
61
+ ...(receiptBase.runner ? { runner: receiptBase.runner } : {}),
62
+ ...(receiptBase.model ? { model: receiptBase.model } : {}),
63
+ ...(receiptBase.session ? { session: receiptBase.session } : {}),
64
+ ...(receiptBase.started ? { started: receiptBase.started } : {}),
65
+ ...(receiptBase.cost_usd !== null ? { cost_usd: receiptBase.cost_usd } : {}),
66
+ ...(failure ? { failure } : {}),
67
+ });
68
+ writeText(path.join(rdir, 'receipt.json'), `${JSON.stringify({ ...receiptBase, status, ...(failure ? { failure } : {}) }, null, 2)}\n`);
69
+ };
70
+ let result;
71
+ try {
72
+ result = parseRoleResult(markdown);
73
+ }
74
+ catch (e) {
75
+ pushRun('failed', 'no-result');
76
+ saveChange(dir, change);
77
+ throw e;
78
+ }
79
+ saveEvidence(dir, change, role, phase, markdown);
80
+ if (role === 'planner' && phase === 'propose') {
81
+ if (result.size)
82
+ change.size = result.size;
83
+ if (result.skip_specs !== undefined) {
84
+ change.skip_specs = result.skip_specs;
85
+ const openspecMeta = path.join(dir, '.openspec.yaml');
86
+ if (exists(openspecMeta))
87
+ writeText(openspecMeta, readText(openspecMeta).replace(/^skip_specs:.*$/m, `skip_specs: ${result.skip_specs}`));
88
+ }
89
+ }
90
+ if (result.complexity) {
91
+ const prev = change.complexity;
92
+ change.complexity = {
93
+ implementation: prev?.implementation === 'высокая' ? 'высокая' : result.complexity.implementation,
94
+ review: prev?.review === 'высокая' ? 'высокая' : result.complexity.review,
95
+ };
96
+ }
97
+ if (role === 'tester' && result.protected && result.protected.length > 0) {
98
+ change.protected = [...new Set([...change.protected, ...result.protected, ...config.testing.protectedGlobs])];
99
+ }
100
+ if (role === 'verifier' && (result.checks || result.gaps)) {
101
+ change.verification = { run: runId, checks: result.checks ?? [], gaps: result.gaps ?? [] };
102
+ // Кандидаты подключения по образцу, которые верификатор не закрыл сам: становятся пунктами PARTIAL для disposition ревьюера.
103
+ const w = wiringForChange(root, dir);
104
+ if (w && w.gaps.length > 0) {
105
+ const covered = new Set(change.verification.checks.flatMap((c) => w.gaps.filter((g) => `${c.purpose ?? ''} ${c.evidence ?? ''}`.includes(g.file)).map((g) => g.file)));
106
+ for (const [i, gap] of w.gaps.filter((g) => !covered.has(g.file)).entries()) {
107
+ change.verification.checks.push({ id: `W${i + 1}`, purpose: `подключение нового модуля «${w.fresh}» в ${gap.file}, где зарегистрирован образец «${w.analog}»`, result: 'PARTIAL', evidence: `sbox wiring: образец упомянут ${gap.analogMentions} раз, новый модуль не упомянут; верификатор этот файл не рассматривал` });
108
+ }
109
+ }
110
+ }
111
+ if (role === 'reviewer' && phase === 'review' && result.dispositions) {
112
+ change.accepted_gaps = result.dispositions.filter((d) => d.disposition === 'manual_gap_accepted').map((d) => ({ item: d.item, ...(d.reason ? { reason: d.reason } : {}) }));
113
+ }
114
+ let phaseCompleted = false;
115
+ let runStatus = 'done';
116
+ const p0 = (result.questions ?? []).filter((q) => q.priority === 'P0');
117
+ const changeDirRel = toPosix(path.relative(root, dir));
118
+ if (result.status === 'заблокировано') {
119
+ runStatus = 'blocked';
120
+ routeBlocker(change, config, { category: result.blocker.category, artifact: result.blocker.artifact, message: result.blocker.message ?? 'без описания', role, phase });
121
+ }
122
+ else if (p0.length > 0) {
123
+ runStatus = 'blocked';
124
+ routeBlocker(change, config, { category: 'пользователь', message: `Вопросы P0: ${p0.map((q) => `${q.id}: ${q.text}`).join(' | ')}`, role, phase });
125
+ }
126
+ else {
127
+ const check = await checkPhaseDone({ ...input, result, changeDirRel });
128
+ diagnostics.push(...check);
129
+ if (hasErrors(check)) {
130
+ runStatus = 'failed';
131
+ }
132
+ else if (role === 'verifier' && (result.checks ?? []).some((c) => c.result === 'FAIL')) {
133
+ runStatus = 'blocked';
134
+ routeBlocker(change, config, {
135
+ category: 'реализация',
136
+ message: `Проверки FAIL: ${(result.checks ?? []).filter((c) => c.result === 'FAIL').map((c) => `${c.id}${c.evidence ? ` (${c.evidence})` : ''}`).join(' | ')}`,
137
+ role,
138
+ phase,
139
+ });
140
+ }
141
+ else if (role === 'reviewer' && (result.findings ?? []).some((f) => f.level === 'blocking')) {
142
+ runStatus = 'blocked';
143
+ routeBlocker(change, config, {
144
+ category: phase === 'tests_review' ? 'тесты' : 'реализация',
145
+ message: (result.findings ?? []).filter((f) => f.level === 'blocking').map((f) => `${f.file ? `${f.file}: ` : ''}${f.text}`).join(' | '),
146
+ role,
147
+ phase,
148
+ });
149
+ }
150
+ else if (role === 'reviewer' && phase === 'review' && (result.dispositions ?? []).some((d) => d.disposition === 'change_required')) {
151
+ runStatus = 'blocked';
152
+ routeBlocker(change, config, {
153
+ category: 'реализация',
154
+ message: (result.dispositions ?? []).filter((d) => d.disposition === 'change_required').map((d) => `${d.item}: ${d.reason ?? ''}`).join(' | '),
155
+ role,
156
+ phase,
157
+ });
158
+ }
159
+ else if (role === 'reviewer' && phase === 'review' && (result.dispositions ?? []).some((d) => d.disposition === 'blocked')) {
160
+ runStatus = 'blocked';
161
+ routeBlocker(change, config, {
162
+ category: 'внешний',
163
+ message: (result.dispositions ?? []).filter((d) => d.disposition === 'blocked').map((d) => `${d.item}: ${d.reason ?? ''}`).join(' | '),
164
+ role,
165
+ phase,
166
+ });
167
+ }
168
+ else {
169
+ if (phase === 'cover' && change.protected.length > 0) {
170
+ change.protected_snapshot = snapshotProtected(root, change.protected);
171
+ diagnostics.push(diag('info', 'PROTECTED_SNAPSHOT', `Снимок защищённых файлов: ${Object.keys(change.protected_snapshot).length}`, 'protected'));
172
+ }
173
+ if (phase === 'implement')
174
+ sealAfterImplement(root, config, dir, change, runId, changeDirRel, diagnostics);
175
+ if ((phase === 'verify' || phase === 'review') && change.changeset)
176
+ change.reviewed_digest = change.changeset.digest;
177
+ completePhase(change, config, phase);
178
+ phaseCompleted = true;
179
+ }
180
+ }
181
+ pushRun(runStatus, runStatus === 'failed' ? 'checks' : undefined);
182
+ saveChange(dir, change);
183
+ return { runId, result, phaseCompleted, diagnostics, next: nextStep(change, config) };
184
+ }
185
+ function sealAfterImplement(root, config, dir, change, runId, changeDirRel, diagnostics) {
186
+ if (!isGitRepo(root) || !change.base_revision) {
187
+ diagnostics.push(diag('warning', 'CHANGESET_SKIPPED', 'Change-set не запечатан: нет git-репозитория или базовой ревизии', 'changeset'));
188
+ return;
189
+ }
190
+ const sealed = sealChangeSet(root, change.base_revision, changesetExclude(config, changeDirRel), runId);
191
+ writeChangeSet(dir, sealed);
192
+ change.changeset = { base: sealed.base, digest: sealed.digest, paths: sealed.paths, sealed_after: runId, sealed_at: sealed.sealed_at, rebound: [] };
193
+ change.reviewed_digest = null;
194
+ diagnostics.push(diag('info', 'CHANGESET_SEALED', `Change-set запечатан: ${sealed.paths} файлов, ${sealed.digest.slice(0, 19)}…`, 'changeset'));
195
+ }
196
+ function saveEvidence(dir, change, role, phase, markdown) {
197
+ const n = change.runs.filter((r) => r.role === role && r.phase === phase).length + 1;
198
+ const names = {
199
+ 'researcher:research': 'research.md',
200
+ 'reviewer:tests_review': `tests-review-${n}.md`,
201
+ 'reviewer:review': `review-${n}.md`,
202
+ 'verifier:verify': `verify-${n}.md`,
203
+ 'challenger:plan': 'challenge.md',
204
+ };
205
+ const name = names[`${role}:${phase}`];
206
+ if (!name)
207
+ return;
208
+ const file = path.join(dir, 'evidence', name);
209
+ if (role === 'researcher' && exists(file))
210
+ return;
211
+ writeText(file, markdown);
212
+ }
213
+ /** Детерминированные проверки завершённости фазы: артефакты, дельты, задачи, защищённые файлы, дайджест, disposition. */
214
+ async function checkPhaseDone(input) {
215
+ const { root, config, dir, change, phase, role, result, adapter, changeDirRel } = input;
216
+ const out = [];
217
+ const workflow = loadWorkflow(root);
218
+ const states = artifactStates(workflow, change, dir);
219
+ if (phase === 'propose' || phase === 'plan' || phase === 'cover') {
220
+ for (const s of pendingArtifacts(states, phase)) {
221
+ if (s.optional)
222
+ continue;
223
+ if (phase === 'cover' && s.id === 'test-plan')
224
+ continue;
225
+ out.push(diag('error', 'ARTIFACT_MISSING', `Артефакт ${s.id} не создан: ожидался ${path.relative(root, s.outputPath)}`, s.id));
226
+ }
227
+ }
228
+ if (phase === 'plan' && !change.skip_specs) {
229
+ try {
230
+ const truth = await adapter.readTruth();
231
+ const deltas = await adapter.readDelta(path.join(dir, 'specs'));
232
+ if (deltas.length === 0)
233
+ out.push(diag('error', 'DELTA_MISSING', 'В specs/ нет ни одной дельты, а skip_specs не задан', 'specs'));
234
+ out.push(...adapter.validate(truth, deltas));
235
+ }
236
+ catch (e) {
237
+ out.push(diag('error', 'DELTA_READ', e.message, 'specs'));
238
+ }
239
+ }
240
+ if (phase === 'implement') {
241
+ const tasksFile = path.join(dir, 'tasks.md');
242
+ if (exists(tasksFile)) {
243
+ const progress = taskProgress(readText(tasksFile));
244
+ if (progress.remaining > 0) {
245
+ out.push(diag('error', 'TASKS_REMAINING', `Не отмечено задач: ${progress.remaining} из ${progress.total}`, 'tasks.md', 'Выполните задачи и отметьте их `- [x]`, либо верните статус «заблокировано».'));
246
+ }
247
+ }
248
+ if (change.protected.length > 0) {
249
+ const hasSnapshot = Object.keys(change.protected_snapshot).length > 0;
250
+ const violations = hasSnapshot
251
+ ? protectedViolations(root, change.protected, change.protected_snapshot).map((v) => `${v.path} (${v.kind === 'modified' ? 'изменён' : v.kind === 'deleted' ? 'удалён' : 'добавлен'})`)
252
+ : trackedChangedFiles(root).filter((f) => picomatch(change.protected, { dot: true })(f)).map((f) => `${f} (изменён)`);
253
+ if (violations.length > 0) {
254
+ out.push(diag('error', 'PROTECTED_CHANGED', `Изменены защищённые файлы: ${violations.join(', ')}`, 'protected', 'Откатите правки тестов; спорный тест возвращайте блокером категории «тесты».'));
255
+ }
256
+ }
257
+ }
258
+ if (phase === 'verify' && config.wiring.strict) {
259
+ const w = wiringForChange(root, dir);
260
+ if (w && w.gaps.length > 0) {
261
+ out.push(diag('error', 'WIRING_MISSING', `Образец «${w.analog}» подключён в файлах, где нового модуля «${w.fresh}» нет: ${w.gaps.map((g) => g.file).join(', ')}`, 'wiring', 'Добавьте регистрацию или перечислите файлы в design.md строкой «Исключения подключения» с причиной; либо выключите wiring.strict.'));
262
+ }
263
+ }
264
+ if ((phase === 'verify' || phase === 'review') && change.changeset && isGitRepo(root)) {
265
+ // Безобидный дрейф (документация, .gitignore, материалы хостов) перепривязывается, код после запечатывания меняться не может.
266
+ out.push(...reconcileDrift(root, config, dir, change, { rebind: true }).diagnostics);
267
+ }
268
+ if (role === 'reviewer' && phase === 'review') {
269
+ const pending = new Set();
270
+ for (const c of change.verification?.checks ?? [])
271
+ if (c.result !== 'PASS')
272
+ pending.add(c.id);
273
+ for (const g of change.verification?.gaps ?? [])
274
+ pending.add(g.id);
275
+ for (const d of result.dispositions ?? [])
276
+ pending.delete(d.item);
277
+ if (pending.size > 0) {
278
+ out.push(diag('error', 'REVIEW_DISPOSITION_MISSING', `Нет disposition для пунктов верификатора: ${[...pending].join(', ')}`, 'dispositions', 'Каждый не-PASS пункт и пробел получает satisfied, manual_gap_accepted, change_required или blocked.'));
279
+ }
280
+ if (result.status === 'готово' && !result.delivery_narrative) {
281
+ out.push(diag('error', 'DELIVERY_NARRATIVE_MISSING', 'Вердикт «готово» требует delivery_narrative с полями title, delta, why', 'delivery_narrative'));
282
+ }
283
+ }
284
+ return out;
285
+ }
286
+ //# sourceMappingURL=report.js.map