kodu 2.1.1 → 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.
- package/AGENTS.md +23 -1
- package/__tests__/core/fs/fs.service.test.ts +72 -0
- package/__tests__/shared/cleaner/cleaner.service.test.ts +102 -0
- package/__tests__/shared/git/git.service.test.ts +84 -0
- package/__tests__/shared/tokenizer/tokenizer.service.test.ts +45 -0
- package/dist/package.json +14 -3
- package/dist/src/commands/init/init.command.d.ts +1 -0
- package/dist/src/commands/init/init.command.js +34 -1
- package/dist/src/commands/init/init.command.js.map +1 -1
- package/dist/src/core/config/config.service.js +2 -4
- package/dist/src/core/config/config.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/lefthook.yml +9 -2
- package/package.json +14 -3
- package/skills/doc-gen/SKILL.md +490 -0
- package/skills/doc-gen/scripts/doc_gen.py +911 -0
- package/skills/implement-project/SKILL.md +409 -0
- package/skills/liteend-init/SKILL.md +84 -0
- package/skills/litefront-init/SKILL.md +96 -0
- package/skills/litefront-prototype/SKILL.md +484 -0
- package/skills/project-setup-standardizer/SKILL.md +285 -0
- package/skills/start/SKILL.md +319 -0
- package/skills/tech-blueprint/SKILL.md +890 -0
- package/skills/tech-blueprint/scripts/blueprint_validator.py +417 -0
- package/src/commands/init/init.command.ts +43 -1
- package/src/core/config/config.service.ts +3 -6
- package/tsconfig.build.json +3 -0
- package/tsconfig.json +5 -2
- package/dist/scripts/generate-json-schema.d.ts +0 -1
- package/dist/scripts/generate-json-schema.js +0 -17
- package/dist/scripts/generate-json-schema.js.map +0 -1
- package/skills/kodu-ops/SKILL.md +0 -184
|
@@ -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
|
-
|
|
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
|
|
|
@@ -19,13 +19,10 @@ export class ConfigService {
|
|
|
19
19
|
const explorer = lilconfigSync('kodu', { searchPlaces: ['kodu.json'] });
|
|
20
20
|
const result = explorer.search(process.cwd());
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'kodu.json not found. Create it in the project root to configure kodu.',
|
|
25
|
-
);
|
|
26
|
-
}
|
|
22
|
+
const rawConfig =
|
|
23
|
+
result && !result.isEmpty && result.config ? result.config : {};
|
|
27
24
|
|
|
28
|
-
const parsed = configSchema.safeParse(
|
|
25
|
+
const parsed = configSchema.safeParse(rawConfig);
|
|
29
26
|
|
|
30
27
|
if (!parsed.success) {
|
|
31
28
|
console.error(pc.red('kodu.json is invalid:'));
|
package/tsconfig.build.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
3
4
|
"module": "nodenext",
|
|
4
5
|
"moduleResolution": "nodenext",
|
|
5
6
|
"resolvePackageJsonExports": true,
|
|
@@ -10,16 +11,18 @@
|
|
|
10
11
|
"emitDecoratorMetadata": true,
|
|
11
12
|
"experimentalDecorators": true,
|
|
12
13
|
"allowSyntheticDefaultImports": true,
|
|
13
|
-
"target": "ES2023",
|
|
14
14
|
"sourceMap": true,
|
|
15
15
|
"outDir": "./dist",
|
|
16
16
|
"baseUrl": "./",
|
|
17
17
|
"incremental": true,
|
|
18
18
|
"skipLibCheck": true,
|
|
19
|
+
"strict": true,
|
|
19
20
|
"strictNullChecks": true,
|
|
20
21
|
"forceConsistentCasingInFileNames": true,
|
|
21
22
|
"noImplicitAny": false,
|
|
23
|
+
"noEmit": true,
|
|
22
24
|
"strictBindCallApply": false,
|
|
23
25
|
"noFallthroughCasesInSwitch": false
|
|
24
|
-
}
|
|
26
|
+
},
|
|
27
|
+
"include": ["src", "*.ts"]
|
|
25
28
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const promises_1 = require("node:fs/promises");
|
|
4
|
-
const execa_1 = require("execa");
|
|
5
|
-
const config_schema_1 = require("../src/core/config/config.schema");
|
|
6
|
-
async function main() {
|
|
7
|
-
const schema = config_schema_1.configSchema.toJSONSchema();
|
|
8
|
-
const schemaString = JSON.stringify(schema, null, 2);
|
|
9
|
-
await (0, promises_1.writeFile)('kodu.schema.json', schemaString, 'utf8');
|
|
10
|
-
await (0, execa_1.execa)('biome', ['format', '--write', 'kodu.schema.json']);
|
|
11
|
-
console.log('✅ JSON schema generated: kodu.schema.json');
|
|
12
|
-
}
|
|
13
|
-
main().catch((error) => {
|
|
14
|
-
console.error(error);
|
|
15
|
-
process.exitCode = 1;
|
|
16
|
-
});
|
|
17
|
-
//# sourceMappingURL=generate-json-schema.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generate-json-schema.js","sourceRoot":"","sources":["../../scripts/generate-json-schema.ts"],"names":[],"mappings":";;AAAA,+CAA6C;AAC7C,iCAA8B;AAC9B,oEAAgE;AAEhE,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,4BAAY,CAAC,YAAY,EAAE,CAAC;IAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAErD,MAAM,IAAA,oBAAS,EAAC,kBAAkB,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,IAAA,aAAK,EAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAEhE,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;AAC3D,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|