kodu 2.1.2 → 2.1.3

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.
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ blueprint_validator.py — валидатор технических контрактов проекта.
4
+
5
+ Использование:
6
+ python3 blueprint_validator.py validate "ИмяПроекта" [--output PATH] [--update-mode]
7
+ """
8
+
9
+ import argparse
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+
15
+ # ─── Цвета и вывод ────────────────────────────────────────────────────────────
16
+
17
+ class C:
18
+ RED = "\033[0;31m"
19
+ GREEN = "\033[0;32m"
20
+ YELLOW = "\033[1;33m"
21
+ RESET = "\033[0m"
22
+
23
+
24
+ def ok(msg: str) -> None: print(f"{C.GREEN}✓{C.RESET} {msg}")
25
+ def err(msg: str) -> None: print(f"{C.RED}✗{C.RESET} {msg}")
26
+ def warn(msg: str) -> None: print(f"{C.YELLOW}⟳{C.RESET} {msg}")
27
+
28
+
29
+ # ─── Константы ────────────────────────────────────────────────────────────────
30
+
31
+ REQUIRED_FILES = [
32
+ "IMPLEMENTATION_GUIDE.md",
33
+ "DATABASE_MODEL.md",
34
+ "API_CONTRACTS.md",
35
+ "ARCHITECTURE.md",
36
+ "TESTING_PLAN.md",
37
+ ]
38
+
39
+ _FSD_PATH_RE = re.compile(
40
+ r"src/(entities|features|widgets|pages|shared|app)/|"
41
+ r"app/(entities|features|widgets|pages|shared)/"
42
+ )
43
+
44
+ _LIST_TYPE_RE = re.compile(r":\s*\[\w")
45
+ _PAGINATION_KW_RE = re.compile(r"\b(first|after|limit|offset|page|cursor)\b", re.IGNORECASE)
46
+ _SPEC_REF_RE = re.compile(r"(SPEC\.md|VISION\.md)")
47
+ _PRISMA_MODEL_RE = re.compile(r"model\s+(\w+)\s*\{([\s\S]*?)\n\}", re.MULTILINE)
48
+ _MIGRATION_SEC_RE = re.compile(
49
+ r"^#{1,3}\s*(план миграции|изменения бд|migration plan|db changes|database changes)",
50
+ re.MULTILINE | re.IGNORECASE,
51
+ )
52
+ _IMPL_STACK_RE = re.compile(r"^#{1,3}\s*(стек|stack)\b", re.MULTILINE | re.IGNORECASE)
53
+ _IMPL_DONE_RE = re.compile(
54
+ r"^#{1,3}\s*(что уже реализовано|already implemented|what.s already)",
55
+ re.MULTILINE | re.IGNORECASE,
56
+ )
57
+ _IMPL_LAUNCH_RE = re.compile(
58
+ r"^#{1,3}\s*(локальный запуск|local (setup|run|start)|getting started|quick start)",
59
+ re.MULTILINE | re.IGNORECASE,
60
+ )
61
+
62
+
63
+ # ─── Вспомогательные функции ──────────────────────────────────────────────────
64
+
65
+ def _read(path: Path) -> str:
66
+ return path.read_text(encoding="utf-8")
67
+
68
+
69
+ def _extract_code_block(content: str, lang: str) -> str:
70
+ """Возвращает содержимое первого блока ```lang ... ```."""
71
+ m = re.search(rf"```{re.escape(lang)}\n([\s\S]*?)```", content)
72
+ return m.group(1) if m else ""
73
+
74
+
75
+ def _extract_prisma_models(prisma_content: str) -> dict[str, str]:
76
+ """Возвращает {ИмяМодели: тело_модели} из Prisma-схемы."""
77
+ return {m.group(1): m.group(2) for m in _PRISMA_MODEL_RE.finditer(prisma_content)}
78
+
79
+
80
+ # ─── Проверки ─────────────────────────────────────────────────────────────────
81
+
82
+ def check_files(blueprint_dir: Path) -> list[str]:
83
+ """Проверка 1: все обязательные файлы существуют."""
84
+ return [
85
+ f"Файл отсутствует: {f}"
86
+ for f in REQUIRED_FILES
87
+ if not (blueprint_dir / f).exists()
88
+ ]
89
+
90
+
91
+ def check_code_blocks(blueprint_dir: Path) -> list[str]:
92
+ """Проверка 2: DATABASE_MODEL.md содержит ```prisma, API_CONTRACTS.md — ```graphql."""
93
+ errors: list[str] = []
94
+ db = _read(blueprint_dir / "DATABASE_MODEL.md")
95
+ if not _extract_code_block(db, "prisma"):
96
+ errors.append("DATABASE_MODEL.md: отсутствует блок ```prisma")
97
+ api = _read(blueprint_dir / "API_CONTRACTS.md")
98
+ if not _extract_code_block(api, "graphql"):
99
+ errors.append("API_CONTRACTS.md: отсутствует блок ```graphql")
100
+ return errors
101
+
102
+
103
+ def check_fsd_paths(blueprint_dir: Path) -> list[str]:
104
+ """Проверка 3: ARCHITECTURE.md не содержит файловых путей FSD."""
105
+ arch = _read(blueprint_dir / "ARCHITECTURE.md")
106
+ matches = _FSD_PATH_RE.findall(arch)
107
+ if matches:
108
+ return [
109
+ "ARCHITECTURE.md: обнаружены файловые пути FSD. "
110
+ "В архитектурном документе должны быть только логические названия "
111
+ "сущностей и компонентов (не файловые пути)."
112
+ ]
113
+ return []
114
+
115
+
116
+ def check_model_coverage(blueprint_dir: Path) -> list[str]:
117
+ """Проверка 4: большинство Prisma-моделей упомянуты в API_CONTRACTS.md."""
118
+ db = _read(blueprint_dir / "DATABASE_MODEL.md")
119
+ api = _read(blueprint_dir / "API_CONTRACTS.md")
120
+
121
+ prisma_block = _extract_code_block(db, "prisma")
122
+ if not prisma_block:
123
+ return []
124
+
125
+ model_names = list(_extract_prisma_models(prisma_block).keys())
126
+ if not model_names:
127
+ return []
128
+
129
+ api_lower = api.lower()
130
+ covered = [m for m in model_names if m.lower() in api_lower]
131
+ ratio = len(covered) / len(model_names)
132
+
133
+ if ratio < 0.5:
134
+ missing = [m for m in model_names if m.lower() not in api_lower]
135
+ return [
136
+ f"Кросс-чек БД/API: {len(covered)}/{len(model_names)} Prisma-моделей "
137
+ f"найдены в API_CONTRACTS.md. Отсутствуют: {', '.join(missing)}"
138
+ ]
139
+ return []
140
+
141
+
142
+ def check_traceability(blueprint_dir: Path) -> list[str]:
143
+ """Проверка 5: DATABASE_MODEL.md и API_CONTRACTS.md содержат ссылки на SPEC.md/VISION.md."""
144
+ errors: list[str] = []
145
+ for filename in ("DATABASE_MODEL.md", "API_CONTRACTS.md"):
146
+ content = _read(blueprint_dir / filename)
147
+ if not _SPEC_REF_RE.search(content):
148
+ errors.append(
149
+ f"{filename}: отсутствует трассируемость. "
150
+ "Добавьте комментарии со ссылками на бизнес-требования (SPEC.md или VISION.md)"
151
+ )
152
+ return errors
153
+
154
+
155
+ def check_pagination(blueprint_dir: Path) -> list[str]:
156
+ """
157
+ Проверка 6: поля Query/Mutation, возвращающие списки, имеют аргументы пагинации.
158
+ Ищет только внутри type Query / type Mutation / type Subscription.
159
+ Предупреждения, не ошибки (pagination может быть в обёртке-типе).
160
+ """
161
+ issues: list[str] = []
162
+ api = _read(blueprint_dir / "API_CONTRACTS.md")
163
+ graphql_block = _extract_code_block(api, "graphql")
164
+ if not graphql_block:
165
+ return issues
166
+
167
+ lines = graphql_block.splitlines()
168
+ in_root_op = False
169
+ current_type: str | None = None
170
+ depth = 0
171
+
172
+ for i, line in enumerate(lines):
173
+ stripped = line.strip()
174
+
175
+ # Определить вход в тип Query/Mutation/Subscription
176
+ m = re.match(r"^type\s+(Query|Mutation|Subscription)\b", stripped)
177
+ if m:
178
+ in_root_op = True
179
+ current_type = m.group(1)
180
+ depth = 0
181
+
182
+ # Обновить глубину вложенности
183
+ depth += stripped.count("{") - stripped.count("}")
184
+ if depth <= 0 and in_root_op:
185
+ in_root_op = False
186
+ current_type = None
187
+ continue
188
+
189
+ if not in_root_op:
190
+ continue
191
+
192
+ # Проверить, возвращает ли строка список
193
+ if not _LIST_TYPE_RE.search(stripped):
194
+ continue
195
+
196
+ # Собрать контекст: до 8 предыдущих строк (для многострочных аргументов)
197
+ ctx = "\n".join(lines[max(0, i - 8) : i + 1])
198
+ if not _PAGINATION_KW_RE.search(ctx):
199
+ name_m = re.search(r"(\w+)\s*[\(:]", stripped)
200
+ field_name = name_m.group(1) if name_m else stripped[:50]
201
+ issues.append(
202
+ f"API_CONTRACTS.md [{current_type}]: поле «{field_name}» возвращает список "
203
+ "без аргументов пагинации — добавьте first/after или limit/offset"
204
+ )
205
+
206
+ return issues
207
+
208
+
209
+ def check_prisma_timestamps(blueprint_dir: Path) -> list[str]:
210
+ """
211
+ Проверка 7: каждая Prisma-модель (кроме join-таблиц с '_' в названии)
212
+ содержит createdAt или updatedAt.
213
+ """
214
+ errors: list[str] = []
215
+ db = _read(blueprint_dir / "DATABASE_MODEL.md")
216
+ prisma_block = _extract_code_block(db, "prisma")
217
+ if not prisma_block:
218
+ return errors
219
+
220
+ for model_name, body in _extract_prisma_models(prisma_block).items():
221
+ # Join-таблицы (содержат '_' в имени) — пропустить
222
+ if "_" in model_name:
223
+ continue
224
+ if "createdAt" not in body and "updatedAt" not in body:
225
+ errors.append(
226
+ f"DATABASE_MODEL.md: модель «{model_name}» не содержит "
227
+ "обязательных полей createdAt/updatedAt"
228
+ )
229
+
230
+ return errors
231
+
232
+
233
+ def check_implementation_guide(blueprint_dir: Path) -> list[str]:
234
+ """Проверка 8: IMPLEMENTATION_GUIDE.md содержит обязательные разделы."""
235
+ guide_path = blueprint_dir / "IMPLEMENTATION_GUIDE.md"
236
+ if not guide_path.exists():
237
+ return [] # уже зафиксировано в check_files
238
+ content = _read(guide_path)
239
+ errors: list[str] = []
240
+ if not _IMPL_STACK_RE.search(content):
241
+ errors.append("IMPLEMENTATION_GUIDE.md: отсутствует раздел «## Стек»")
242
+ if not _IMPL_DONE_RE.search(content):
243
+ errors.append("IMPLEMENTATION_GUIDE.md: отсутствует раздел «## Что уже реализовано»")
244
+ if not _IMPL_LAUNCH_RE.search(content):
245
+ errors.append("IMPLEMENTATION_GUIDE.md: отсутствует раздел «## Локальный запуск»")
246
+ return errors
247
+
248
+
249
+ def check_migration_section(blueprint_dir: Path) -> list[str]:
250
+ """Проверка 8 (--update-mode): DATABASE_MODEL.md содержит раздел «План миграции»."""
251
+ db = _read(blueprint_dir / "DATABASE_MODEL.md")
252
+ if not _MIGRATION_SEC_RE.search(db):
253
+ return [
254
+ "DATABASE_MODEL.md: в режиме обновления требуется описать план миграции. "
255
+ "Добавьте раздел «## План миграции» или «## Изменения БД»"
256
+ ]
257
+ return []
258
+
259
+
260
+ # ─── Команда validate ──────────────────────────────────────────────────────────
261
+
262
+ def cmd_validate(args: argparse.Namespace) -> int:
263
+ blueprint_dir = Path(args.output) / args.name / "3_TECH_BLUEPRINT"
264
+
265
+ if not blueprint_dir.exists():
266
+ err(f"Папка не найдена: {blueprint_dir}")
267
+ print(f" Ожидается: docs/{args.name}/3_TECH_BLUEPRINT/")
268
+ return 1
269
+
270
+ print(f"\nВалидация технического блюпринта: {args.name}\n")
271
+
272
+ all_errors: list[str] = []
273
+ all_warnings: list[str] = []
274
+
275
+ # ── 1. Наличие файлов ─────────────────────────────────────────────────────
276
+ print(f"{C.YELLOW}── Наличие файлов ──────────────────────────{C.RESET}")
277
+ file_errors = check_files(blueprint_dir)
278
+ for e in file_errors:
279
+ err(e)
280
+ if file_errors:
281
+ print(
282
+ f"\n{C.RED}Критические ошибки: создайте все обязательные файлы "
283
+ f"прежде чем продолжить.{C.RESET}\n"
284
+ )
285
+ return 1
286
+ ok("Все обязательные файлы присутствуют")
287
+
288
+ # ── 2. Блоки кода ─────────────────────────────────────────────────────────
289
+ print(f"\n{C.YELLOW}── Структура контента ──────────────────────{C.RESET}")
290
+ block_errors = check_code_blocks(blueprint_dir)
291
+ for e in block_errors:
292
+ err(e)
293
+ all_errors.extend(block_errors)
294
+ if not block_errors:
295
+ ok("Блоки кода (```prisma, ```graphql) найдены")
296
+
297
+ # ── 3. FSD-пути ───────────────────────────────────────────────────────────
298
+ print(f"\n{C.YELLOW}── FSD Paths Check ─────────────────────────{C.RESET}")
299
+ fsd_errors = check_fsd_paths(blueprint_dir)
300
+ for e in fsd_errors:
301
+ err(e)
302
+ all_errors.extend(fsd_errors)
303
+ if not fsd_errors:
304
+ ok("ARCHITECTURE.md не содержит файловых путей FSD")
305
+
306
+ # ── 4. Кросс-чек моделей ──────────────────────────────────────────────────
307
+ print(f"\n{C.YELLOW}── Кросс-чек БД / GraphQL ──────────────────{C.RESET}")
308
+ if not block_errors:
309
+ coverage_errors = check_model_coverage(blueprint_dir)
310
+ for e in coverage_errors:
311
+ err(e)
312
+ all_errors.extend(coverage_errors)
313
+ if not coverage_errors:
314
+ ok("Большинство Prisma-моделей упомянуты в API_CONTRACTS.md")
315
+
316
+ # ── 5. Трассируемость ─────────────────────────────────────────────────────
317
+ print(f"\n{C.YELLOW}── Трассируемость ──────────────────────────{C.RESET}")
318
+ trace_errors = check_traceability(blueprint_dir)
319
+ for e in trace_errors:
320
+ err(e)
321
+ all_errors.extend(trace_errors)
322
+ if not trace_errors:
323
+ ok("Ссылки на бизнес-требования найдены в DB-модели и API-контрактах")
324
+
325
+ # ── 6. Пагинация GraphQL ──────────────────────────────────────────────────
326
+ print(f"\n{C.YELLOW}── Пагинация GraphQL ───────────────────────{C.RESET}")
327
+ if not block_errors:
328
+ pag_issues = check_pagination(blueprint_dir)
329
+ for issue in pag_issues:
330
+ warn(issue)
331
+ all_warnings.extend(pag_issues)
332
+ if not pag_issues:
333
+ ok("Все списочные поля Query/Mutation имеют аргументы пагинации")
334
+
335
+ # ── 7. Технические поля Prisma ────────────────────────────────────────────
336
+ print(f"\n{C.YELLOW}── Технические поля Prisma ─────────────────{C.RESET}")
337
+ if not block_errors:
338
+ ts_errors = check_prisma_timestamps(blueprint_dir)
339
+ for e in ts_errors:
340
+ err(e)
341
+ all_errors.extend(ts_errors)
342
+ if not ts_errors:
343
+ ok("Все модели (кроме join-таблиц) содержат createdAt/updatedAt")
344
+
345
+ # ── 8. IMPLEMENTATION_GUIDE.md ────────────────────────────────────────────
346
+ print(f"\n{C.YELLOW}── IMPLEMENTATION_GUIDE.md ─────────────────{C.RESET}")
347
+ guide_errors = check_implementation_guide(blueprint_dir)
348
+ for e in guide_errors:
349
+ err(e)
350
+ all_errors.extend(guide_errors)
351
+ if not guide_errors:
352
+ ok("IMPLEMENTATION_GUIDE.md содержит обязательные разделы")
353
+
354
+ # ── 9. Режим обновления ───────────────────────────────────────────────────
355
+ if args.update_mode:
356
+ print(f"\n{C.YELLOW}── Режим обновления (миграция) ─────────────{C.RESET}")
357
+ mig_errors = check_migration_section(blueprint_dir)
358
+ for e in mig_errors:
359
+ err(e)
360
+ all_errors.extend(mig_errors)
361
+ if not mig_errors:
362
+ ok("Раздел с планом миграции найден")
363
+
364
+ # ── Итог ──────────────────────────────────────────────────────────────────
365
+ print()
366
+ if all_errors:
367
+ warn_suffix = f", предупреждений: {len(all_warnings)}" if all_warnings else ""
368
+ print(
369
+ f"{C.RED}Итог: {len(all_errors)} ошибок{warn_suffix}. "
370
+ f"Устраните ошибки перед продолжением.{C.RESET}\n"
371
+ )
372
+ return 1
373
+
374
+ if all_warnings:
375
+ print(
376
+ f"{C.YELLOW}Итог: ошибок нет, предупреждений: {len(all_warnings)}. "
377
+ f"Рекомендуется исправить.{C.RESET}\n"
378
+ )
379
+ else:
380
+ print(
381
+ f"{C.GREEN}✅ Блюпринт «{args.name}» прошёл полную проверку.{C.RESET}\n"
382
+ )
383
+
384
+ return 0
385
+
386
+
387
+ # ─── CLI ──────────────────────────────────────────────────────────────────────
388
+
389
+ def build_parser() -> argparse.ArgumentParser:
390
+ parser = argparse.ArgumentParser(
391
+ prog="blueprint_validator.py",
392
+ description="Валидатор технических контрактов (tech-blueprint)",
393
+ )
394
+ sub = parser.add_subparsers(dest="command", required=True)
395
+
396
+ val = sub.add_parser("validate", help="Проверить папку 3_TECH_BLUEPRINT/")
397
+ val.add_argument("name", metavar="ИмяПроекта")
398
+ val.add_argument(
399
+ "--output", default="./blueprint", metavar="PATH",
400
+ help="Корневая папка проектов (по умолчанию: ./blueprint)",
401
+ )
402
+ val.add_argument(
403
+ "--update-mode", action="store_true",
404
+ help="Режим обновления: требует раздел «План миграции» в DATABASE_MODEL.md",
405
+ )
406
+
407
+ return parser
408
+
409
+
410
+ def main() -> None:
411
+ parser = build_parser()
412
+ args = parser.parse_args()
413
+ sys.exit(cmd_validate(args))
414
+
415
+
416
+ if __name__ == "__main__":
417
+ main()
@@ -5,17 +5,59 @@ import { UiService } from '../../core/ui/ui.service';
5
5
 
6
6
  const GITIGNORE_ENTRY = '.kodu/context.txt';
7
7
 
8
- @Command({ name: 'init', description: 'Add kodu output to .gitignore' })
8
+ const DEFAULT_KODU_JSON = {
9
+ $schema:
10
+ 'https://raw.githubusercontent.com/anomalyco/kodu/main/kodu.schema.json',
11
+ cleaner: {
12
+ whitelist: ['//!'],
13
+ keepJSDoc: true,
14
+ useGitignore: true,
15
+ ignore: [],
16
+ },
17
+ packer: {
18
+ ignore: [
19
+ 'package-lock.json',
20
+ 'yarn.lock',
21
+ 'pnpm-lock.yaml',
22
+ '.git',
23
+ '.kodu',
24
+ 'node_modules',
25
+ 'dist',
26
+ 'coverage',
27
+ ],
28
+ useGitignore: true,
29
+ contentBasedBinaryDetection: false,
30
+ },
31
+ };
32
+
33
+ @Command({ name: 'init', description: 'Initialize kodu configuration' })
9
34
  export class InitCommand extends CommandRunner {
10
35
  constructor(private readonly ui: UiService) {
11
36
  super();
12
37
  }
13
38
 
14
39
  async run(): Promise<void> {
40
+ await this.ensureKoduJson();
15
41
  await this.updateGitignore();
16
42
  this.ui.log.success('Done.');
17
43
  }
18
44
 
45
+ private async ensureKoduJson(): Promise<void> {
46
+ const configPath = path.join(process.cwd(), 'kodu.json');
47
+
48
+ if (await this.exists(configPath)) {
49
+ this.ui.log.info('kodu.json already exists');
50
+ return;
51
+ }
52
+
53
+ await fs.writeFile(
54
+ configPath,
55
+ `${JSON.stringify(DEFAULT_KODU_JSON, null, 2)}\n`,
56
+ 'utf8',
57
+ );
58
+ this.ui.log.success('Created kodu.json');
59
+ }
60
+
19
61
  private async updateGitignore(): Promise<void> {
20
62
  const gitignorePath = path.join(process.cwd(), '.gitignore');
21
63