rpr-cli 0.1.1__py3-none-any.whl
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.
- rpr/__init__.py +1 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +427 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +543 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +89 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +170 -0
- rpr/cli.py +78 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +166 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +228 -0
- rpr/commands/generate/domain.py +383 -0
- rpr/commands/generate/engine.py +148 -0
- rpr/commands/generate/storybook.py +442 -0
- rpr/commands/generate/ui.py +414 -0
- rpr/commands/init.py +822 -0
- rpr/commands/map.py +113 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +97 -0
- rpr/context.py +203 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +110 -0
- rpr/generators/claude.py +33 -0
- rpr/generators/copilot.py +36 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +76 -0
- rpr/scaffolds/domain/settings.md +57 -0
- rpr/scaffolds/instructions/all.instructions.md +50 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +93 -0
- rpr/scaffolds/instructions/frontend.instructions.md +97 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
- rpr/scaffolds/instructions/tooling.instructions.md +42 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
- rpr/scaffolds/special_files/domain_container.md +76 -0
- rpr/scaffolds/special_files/domain_settings.md +57 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +98 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +81 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +131 -0
- rpr_cli-0.1.1.dist-info/METADATA +201 -0
- rpr_cli-0.1.1.dist-info/RECORD +97 -0
- rpr_cli-0.1.1.dist-info/WHEEL +4 -0
- rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
- rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
rpr/checks/packages.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rpr.checks.base import CheckResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DOMAIN_RUNTIME_DEPENDENCIES = {
|
|
11
|
+
"sqlmodel",
|
|
12
|
+
"httpx",
|
|
13
|
+
"dependency-injector",
|
|
14
|
+
"pydantic",
|
|
15
|
+
"pydantic-settings",
|
|
16
|
+
"asyncpg",
|
|
17
|
+
}
|
|
18
|
+
_API_RUNTIME_DEPENDENCIES = {
|
|
19
|
+
"fastapi>=0.115",
|
|
20
|
+
"uvicorn[standard]>=0.34",
|
|
21
|
+
}
|
|
22
|
+
_UI_LIBRARY_DEPENDENCIES = {
|
|
23
|
+
"@mui/material",
|
|
24
|
+
"@emotion/react",
|
|
25
|
+
"@emotion/styled",
|
|
26
|
+
"@tanstack/react-query",
|
|
27
|
+
"@tanstack/react-router",
|
|
28
|
+
"react",
|
|
29
|
+
"react-dom",
|
|
30
|
+
}
|
|
31
|
+
_UI_LIBRARY_DEV_DEPENDENCIES = {
|
|
32
|
+
"@types/react",
|
|
33
|
+
"@types/react-dom",
|
|
34
|
+
"typescript",
|
|
35
|
+
"vite",
|
|
36
|
+
"vitest",
|
|
37
|
+
}
|
|
38
|
+
_WEB_APP_DEPENDENCIES = {"react", "react-dom"}
|
|
39
|
+
_WEB_APP_DEV_DEPENDENCIES = {
|
|
40
|
+
"@types/react",
|
|
41
|
+
"@types/react-dom",
|
|
42
|
+
"@vitejs/plugin-react",
|
|
43
|
+
"typescript",
|
|
44
|
+
"vite",
|
|
45
|
+
"vitest",
|
|
46
|
+
}
|
|
47
|
+
_STORYBOOK_DEPENDENCIES = {
|
|
48
|
+
"@emotion/react",
|
|
49
|
+
"@emotion/styled",
|
|
50
|
+
"@mui/material",
|
|
51
|
+
"react",
|
|
52
|
+
"react-dom",
|
|
53
|
+
}
|
|
54
|
+
_STORYBOOK_DEV_DEPENDENCIES = {
|
|
55
|
+
"@storybook/addon-essentials",
|
|
56
|
+
"@storybook/react",
|
|
57
|
+
"@storybook/react-vite",
|
|
58
|
+
"@types/react",
|
|
59
|
+
"@types/react-dom",
|
|
60
|
+
"storybook",
|
|
61
|
+
"typescript",
|
|
62
|
+
"vite",
|
|
63
|
+
"vitest",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_packages(project_dir: Path, variables: dict[str, str]) -> list[CheckResult]:
|
|
68
|
+
results: list[CheckResult] = []
|
|
69
|
+
name = variables.get("name", "<name>")
|
|
70
|
+
name_underscore = variables.get("name_underscore", name.replace("-", "_"))
|
|
71
|
+
|
|
72
|
+
pyproject = project_dir / "pyproject.toml"
|
|
73
|
+
workspace_members: set[str] = set()
|
|
74
|
+
if pyproject.exists():
|
|
75
|
+
try:
|
|
76
|
+
data = tomllib.loads(pyproject.read_text())
|
|
77
|
+
members = data.get("tool", {}).get("uv", {}).get("workspace", {}).get(
|
|
78
|
+
"members", []
|
|
79
|
+
)
|
|
80
|
+
if isinstance(members, list):
|
|
81
|
+
workspace_members = set(members)
|
|
82
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
83
|
+
workspace_members = set()
|
|
84
|
+
|
|
85
|
+
# Domain package
|
|
86
|
+
domain_dir = project_dir / "packages" / "python" / "domain"
|
|
87
|
+
results.append(
|
|
88
|
+
CheckResult(
|
|
89
|
+
group="Generated Packages & Apps",
|
|
90
|
+
area="Domain package",
|
|
91
|
+
status=domain_dir.exists(),
|
|
92
|
+
path=str(domain_dir)
|
|
93
|
+
if domain_dir.exists()
|
|
94
|
+
else "packages/python/domain/ missing",
|
|
95
|
+
suggestion=f"rpr generate domain {name}",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
if domain_dir.exists():
|
|
99
|
+
results.extend(
|
|
100
|
+
_check_python_layout(
|
|
101
|
+
area="Domain layout",
|
|
102
|
+
project_dir=domain_dir,
|
|
103
|
+
src_package=domain_dir / "src" / f"{name_underscore}_domain",
|
|
104
|
+
suggestion=f"rpr generate domain {name}",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
results.append(
|
|
108
|
+
_check_project_targets(
|
|
109
|
+
area="Domain targets",
|
|
110
|
+
project_json=domain_dir / "project.json",
|
|
111
|
+
required_targets={"build", "test", "lint"},
|
|
112
|
+
suggestion=f"rpr generate domain {name}",
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
results.append(
|
|
116
|
+
_check_project_name(
|
|
117
|
+
area="Domain project name",
|
|
118
|
+
project_json=domain_dir / "project.json",
|
|
119
|
+
expected_name="domain",
|
|
120
|
+
suggestion=f"rpr generate domain {name}",
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
results.append(
|
|
124
|
+
_check_python_dependency_contract(
|
|
125
|
+
area="Domain install metadata",
|
|
126
|
+
pyproject=domain_dir / "pyproject.toml",
|
|
127
|
+
required_dependencies=_DOMAIN_RUNTIME_DEPENDENCIES,
|
|
128
|
+
suggestion=f"rpr generate domain {name}",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
results.append(
|
|
132
|
+
_check_domain_migration_scaffold(
|
|
133
|
+
domain_dir=domain_dir,
|
|
134
|
+
name=name,
|
|
135
|
+
name_underscore=name_underscore,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
results.append(
|
|
139
|
+
CheckResult(
|
|
140
|
+
group="Generated Packages & Apps",
|
|
141
|
+
area="Domain workspace member",
|
|
142
|
+
status="packages/python/domain" in workspace_members,
|
|
143
|
+
path="packages/python/domain"
|
|
144
|
+
if "packages/python/domain" in workspace_members
|
|
145
|
+
else "packages/python/domain missing from [tool.uv.workspace].members",
|
|
146
|
+
suggestion=f"rpr generate domain {name}",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# API app
|
|
151
|
+
api_dir = project_dir / "apps" / f"{name}-api"
|
|
152
|
+
results.append(
|
|
153
|
+
CheckResult(
|
|
154
|
+
group="Generated Packages & Apps",
|
|
155
|
+
area="API app",
|
|
156
|
+
status=api_dir.exists(),
|
|
157
|
+
path=str(api_dir) if api_dir.exists() else f"apps/{name}-api/ missing",
|
|
158
|
+
suggestion=f"rpr generate api {name}",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
if api_dir.exists():
|
|
162
|
+
results.extend(
|
|
163
|
+
_check_python_layout(
|
|
164
|
+
area="API layout",
|
|
165
|
+
project_dir=api_dir,
|
|
166
|
+
src_package=api_dir / "src" / f"{name_underscore}_api",
|
|
167
|
+
suggestion=f"rpr generate api {name}",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
results.append(
|
|
171
|
+
_check_project_targets(
|
|
172
|
+
area="API targets",
|
|
173
|
+
project_json=api_dir / "project.json",
|
|
174
|
+
required_targets={"serve", "build", "test", "lint"},
|
|
175
|
+
suggestion=f"rpr generate api {name}",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
results.append(
|
|
179
|
+
_check_python_dependency_contract(
|
|
180
|
+
area="API install metadata",
|
|
181
|
+
pyproject=api_dir / "pyproject.toml",
|
|
182
|
+
required_dependencies={f"{name}-domain", *_API_RUNTIME_DEPENDENCIES},
|
|
183
|
+
suggestion=f"rpr generate api {name}",
|
|
184
|
+
required_sources={f"{name}-domain": {"workspace": True}},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
results.append(
|
|
188
|
+
CheckResult(
|
|
189
|
+
group="Generated Packages & Apps",
|
|
190
|
+
area="API workspace member",
|
|
191
|
+
status=f"apps/{name}-api" in workspace_members,
|
|
192
|
+
path=f"apps/{name}-api"
|
|
193
|
+
if f"apps/{name}-api" in workspace_members
|
|
194
|
+
else f"apps/{name}-api missing from [tool.uv.workspace].members",
|
|
195
|
+
suggestion=f"rpr generate api {name}",
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# UI library
|
|
200
|
+
ui_dir = project_dir / "packages" / "node" / f"{name}-ui"
|
|
201
|
+
results.append(
|
|
202
|
+
CheckResult(
|
|
203
|
+
group="Generated Packages & Apps",
|
|
204
|
+
area="UI library",
|
|
205
|
+
status=ui_dir.exists(),
|
|
206
|
+
path=str(ui_dir)
|
|
207
|
+
if ui_dir.exists()
|
|
208
|
+
else f"packages/node/{name}-ui/ missing",
|
|
209
|
+
suggestion=f"rpr generate ui {name}",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
if ui_dir.exists():
|
|
213
|
+
results.append(
|
|
214
|
+
_check_project_targets(
|
|
215
|
+
area="UI targets",
|
|
216
|
+
project_json=ui_dir / "project.json",
|
|
217
|
+
required_targets={"build", "lint", "test", "typecheck"},
|
|
218
|
+
suggestion=f"rpr generate ui {name}",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
results.append(
|
|
222
|
+
_check_node_dependency_contract(
|
|
223
|
+
area="UI install metadata",
|
|
224
|
+
package_json=ui_dir / "package.json",
|
|
225
|
+
required_dependencies=_UI_LIBRARY_DEPENDENCIES,
|
|
226
|
+
required_dev_dependencies=_UI_LIBRARY_DEV_DEPENDENCIES,
|
|
227
|
+
suggestion=f"rpr generate ui {name}",
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Web app
|
|
232
|
+
web_dir = project_dir / "apps" / f"{name}-web"
|
|
233
|
+
results.append(
|
|
234
|
+
CheckResult(
|
|
235
|
+
group="Generated Packages & Apps",
|
|
236
|
+
area="Web app",
|
|
237
|
+
status=web_dir.exists(),
|
|
238
|
+
path=str(web_dir) if web_dir.exists() else f"apps/{name}-web/ missing",
|
|
239
|
+
suggestion=f"rpr generate ui {name} --no-library",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
if web_dir.exists():
|
|
243
|
+
results.append(
|
|
244
|
+
_check_project_targets(
|
|
245
|
+
area="Web targets",
|
|
246
|
+
project_json=web_dir / "project.json",
|
|
247
|
+
required_targets={"serve", "build", "lint", "test", "typecheck"},
|
|
248
|
+
suggestion=f"rpr generate ui {name} --no-library",
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
results.append(
|
|
252
|
+
_check_node_dependency_contract(
|
|
253
|
+
area="Web install metadata",
|
|
254
|
+
package_json=web_dir / "package.json",
|
|
255
|
+
required_dependencies={f"@repo/{name}-ui", *_WEB_APP_DEPENDENCIES},
|
|
256
|
+
required_dev_dependencies=_WEB_APP_DEV_DEPENDENCIES,
|
|
257
|
+
suggestion=f"rpr generate ui {name} --no-library",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Storybook
|
|
262
|
+
sb_dir = project_dir / "packages" / "node" / "sb"
|
|
263
|
+
results.append(
|
|
264
|
+
CheckResult(
|
|
265
|
+
group="Generated Packages & Apps",
|
|
266
|
+
area="Storybook",
|
|
267
|
+
status=sb_dir.exists(),
|
|
268
|
+
path=str(sb_dir) if sb_dir.exists() else "packages/node/sb/ missing",
|
|
269
|
+
suggestion="rpr generate storybook",
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
if sb_dir.exists():
|
|
273
|
+
results.append(
|
|
274
|
+
_check_project_targets(
|
|
275
|
+
area="Storybook targets",
|
|
276
|
+
project_json=sb_dir / "project.json",
|
|
277
|
+
required_targets={
|
|
278
|
+
"serve",
|
|
279
|
+
"build",
|
|
280
|
+
"lint",
|
|
281
|
+
"test",
|
|
282
|
+
"typecheck",
|
|
283
|
+
"storybook",
|
|
284
|
+
"build-storybook",
|
|
285
|
+
},
|
|
286
|
+
suggestion="rpr generate storybook",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
results.append(
|
|
290
|
+
_check_node_dependency_contract(
|
|
291
|
+
area="Storybook host install metadata",
|
|
292
|
+
package_json=sb_dir / "package.json",
|
|
293
|
+
required_dependencies={f"@repo/{name}-ui", *_STORYBOOK_DEPENDENCIES},
|
|
294
|
+
required_dev_dependencies=_STORYBOOK_DEV_DEPENDENCIES,
|
|
295
|
+
suggestion="rpr generate storybook",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
results.append(
|
|
299
|
+
_check_node_dependency_contract(
|
|
300
|
+
area="Storybook root install metadata",
|
|
301
|
+
package_json=project_dir / "package.json",
|
|
302
|
+
required_dev_dependencies={"@nx/storybook"},
|
|
303
|
+
suggestion="rpr generate storybook",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Engine
|
|
308
|
+
engine_dir = next(project_dir.glob("packages/python/*-engine"), None)
|
|
309
|
+
results.append(
|
|
310
|
+
CheckResult(
|
|
311
|
+
group="Generated Packages & Apps",
|
|
312
|
+
area="Engine (optional)",
|
|
313
|
+
status=True,
|
|
314
|
+
path=str(engine_dir) if engine_dir else "not scaffolded",
|
|
315
|
+
suggestion=f"rpr generate engine {name}",
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
if engine_dir is not None:
|
|
319
|
+
results.append(
|
|
320
|
+
_check_engine_install_metadata(
|
|
321
|
+
engine_dir=engine_dir,
|
|
322
|
+
suggestion=f"rpr generate engine {name}",
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return results
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _load_json(path: Path) -> dict[str, object] | None:
|
|
330
|
+
if not path.exists():
|
|
331
|
+
return None
|
|
332
|
+
try:
|
|
333
|
+
data = json.loads(path.read_text())
|
|
334
|
+
except (OSError, json.JSONDecodeError):
|
|
335
|
+
return None
|
|
336
|
+
return data if isinstance(data, dict) else None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _load_toml(path: Path) -> dict[str, object] | None:
|
|
340
|
+
if not path.exists():
|
|
341
|
+
return None
|
|
342
|
+
try:
|
|
343
|
+
data = tomllib.loads(path.read_text())
|
|
344
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
345
|
+
return None
|
|
346
|
+
return data if isinstance(data, dict) else None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _check_python_layout(
|
|
350
|
+
*, area: str, project_dir: Path, src_package: Path, suggestion: str
|
|
351
|
+
) -> list[CheckResult]:
|
|
352
|
+
tests_dir = project_dir / "tests"
|
|
353
|
+
return [
|
|
354
|
+
CheckResult(
|
|
355
|
+
group="Generated Packages & Apps",
|
|
356
|
+
area=area,
|
|
357
|
+
status=src_package.exists() and tests_dir.exists(),
|
|
358
|
+
path=str(project_dir)
|
|
359
|
+
if src_package.exists() and tests_dir.exists()
|
|
360
|
+
else f"{project_dir} missing src/ package or tests/",
|
|
361
|
+
suggestion=suggestion,
|
|
362
|
+
)
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _check_project_targets(
|
|
367
|
+
*, area: str, project_json: Path, required_targets: set[str], suggestion: str
|
|
368
|
+
) -> CheckResult:
|
|
369
|
+
data = _load_json(project_json)
|
|
370
|
+
if data is None:
|
|
371
|
+
return CheckResult(
|
|
372
|
+
group="Generated Packages & Apps",
|
|
373
|
+
area=area,
|
|
374
|
+
status=False,
|
|
375
|
+
path=f"{project_json} missing or invalid",
|
|
376
|
+
suggestion=suggestion,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
targets = data.get("targets", {})
|
|
380
|
+
if not isinstance(targets, dict):
|
|
381
|
+
targets = {}
|
|
382
|
+
missing = sorted(required_targets.difference(targets.keys()))
|
|
383
|
+
|
|
384
|
+
return CheckResult(
|
|
385
|
+
group="Generated Packages & Apps",
|
|
386
|
+
area=area,
|
|
387
|
+
status=not missing,
|
|
388
|
+
path=str(project_json)
|
|
389
|
+
if not missing
|
|
390
|
+
else f"{project_json} missing targets: {', '.join(missing)}",
|
|
391
|
+
suggestion=suggestion,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _check_project_name(
|
|
396
|
+
*, area: str, project_json: Path, expected_name: str, suggestion: str
|
|
397
|
+
) -> CheckResult:
|
|
398
|
+
data = _load_json(project_json)
|
|
399
|
+
if data is None:
|
|
400
|
+
return CheckResult(
|
|
401
|
+
group="Generated Packages & Apps",
|
|
402
|
+
area=area,
|
|
403
|
+
status=False,
|
|
404
|
+
path=f"{project_json} missing or invalid",
|
|
405
|
+
suggestion=suggestion,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
project_name = data.get("name")
|
|
409
|
+
status = project_name == expected_name
|
|
410
|
+
return CheckResult(
|
|
411
|
+
group="Generated Packages & Apps",
|
|
412
|
+
area=area,
|
|
413
|
+
status=status,
|
|
414
|
+
path=str(project_json)
|
|
415
|
+
if status
|
|
416
|
+
else f"{project_json} expected name '{expected_name}'",
|
|
417
|
+
suggestion=suggestion,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _check_python_dependency_contract(
|
|
422
|
+
*,
|
|
423
|
+
area: str,
|
|
424
|
+
pyproject: Path,
|
|
425
|
+
required_dependencies: set[str],
|
|
426
|
+
suggestion: str,
|
|
427
|
+
required_sources: dict[str, object] | None = None,
|
|
428
|
+
) -> CheckResult:
|
|
429
|
+
data = _load_toml(pyproject)
|
|
430
|
+
if data is None:
|
|
431
|
+
return CheckResult(
|
|
432
|
+
group="Generated Packages & Apps",
|
|
433
|
+
area=area,
|
|
434
|
+
status=False,
|
|
435
|
+
path=f"{pyproject} missing or invalid",
|
|
436
|
+
suggestion=suggestion,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
project = data.get("project", {}) if isinstance(data.get("project", {}), dict) else {}
|
|
440
|
+
dependencies = project.get("dependencies", []) if isinstance(project, dict) else []
|
|
441
|
+
dependency_set = set(dependencies) if isinstance(dependencies, list) else set()
|
|
442
|
+
missing_dependencies = sorted(required_dependencies.difference(dependency_set))
|
|
443
|
+
|
|
444
|
+
sources = (
|
|
445
|
+
data.get("tool", {}).get("uv", {}).get("sources", {})
|
|
446
|
+
if isinstance(data.get("tool", {}), dict)
|
|
447
|
+
else {}
|
|
448
|
+
)
|
|
449
|
+
source_errors: list[str] = []
|
|
450
|
+
if required_sources and isinstance(sources, dict):
|
|
451
|
+
for key, expected in required_sources.items():
|
|
452
|
+
if sources.get(key) != expected:
|
|
453
|
+
source_errors.append(key)
|
|
454
|
+
elif required_sources:
|
|
455
|
+
source_errors.extend(sorted(required_sources))
|
|
456
|
+
|
|
457
|
+
status = not missing_dependencies and not source_errors
|
|
458
|
+
details: list[str] = []
|
|
459
|
+
if missing_dependencies:
|
|
460
|
+
details.append(f"missing dependencies: {', '.join(missing_dependencies)}")
|
|
461
|
+
if source_errors:
|
|
462
|
+
details.append(f"missing uv sources: {', '.join(source_errors)}")
|
|
463
|
+
|
|
464
|
+
return CheckResult(
|
|
465
|
+
group="Generated Packages & Apps",
|
|
466
|
+
area=area,
|
|
467
|
+
status=status,
|
|
468
|
+
path=str(pyproject)
|
|
469
|
+
if status
|
|
470
|
+
else f"{pyproject} {'; '.join(details)}",
|
|
471
|
+
suggestion=suggestion,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _check_node_dependency_contract(
|
|
476
|
+
*,
|
|
477
|
+
area: str,
|
|
478
|
+
package_json: Path,
|
|
479
|
+
suggestion: str,
|
|
480
|
+
required_dependencies: set[str] | None = None,
|
|
481
|
+
required_dev_dependencies: set[str] | None = None,
|
|
482
|
+
) -> CheckResult:
|
|
483
|
+
data = _load_json(package_json)
|
|
484
|
+
if data is None:
|
|
485
|
+
return CheckResult(
|
|
486
|
+
group="Generated Packages & Apps",
|
|
487
|
+
area=area,
|
|
488
|
+
status=False,
|
|
489
|
+
path=f"{package_json} missing or invalid",
|
|
490
|
+
suggestion=suggestion,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
dependencies = data.get("dependencies", {})
|
|
494
|
+
dev_dependencies = data.get("devDependencies", {})
|
|
495
|
+
dependency_keys = set(dependencies) if isinstance(dependencies, dict) else set()
|
|
496
|
+
dev_dependency_keys = (
|
|
497
|
+
set(dev_dependencies) if isinstance(dev_dependencies, dict) else set()
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
missing_dependencies = sorted(
|
|
501
|
+
(required_dependencies or set()).difference(dependency_keys)
|
|
502
|
+
)
|
|
503
|
+
missing_dev_dependencies = sorted(
|
|
504
|
+
(required_dev_dependencies or set()).difference(dev_dependency_keys)
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
status = not missing_dependencies and not missing_dev_dependencies
|
|
508
|
+
details: list[str] = []
|
|
509
|
+
if missing_dependencies:
|
|
510
|
+
details.append(f"missing dependencies: {', '.join(missing_dependencies)}")
|
|
511
|
+
if missing_dev_dependencies:
|
|
512
|
+
details.append(
|
|
513
|
+
f"missing devDependencies: {', '.join(missing_dev_dependencies)}"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return CheckResult(
|
|
517
|
+
group="Generated Packages & Apps",
|
|
518
|
+
area=area,
|
|
519
|
+
status=status,
|
|
520
|
+
path=str(package_json)
|
|
521
|
+
if status
|
|
522
|
+
else f"{package_json} {'; '.join(details)}",
|
|
523
|
+
suggestion=suggestion,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _check_domain_migration_scaffold(
|
|
528
|
+
*, domain_dir: Path, name: str, name_underscore: str
|
|
529
|
+
) -> CheckResult:
|
|
530
|
+
project_json = domain_dir / "project.json"
|
|
531
|
+
project = _load_json(project_json) or {}
|
|
532
|
+
targets = project.get("targets", {}) if isinstance(project.get("targets", {}), dict) else {}
|
|
533
|
+
migrate_target = targets.get("migrate") if isinstance(targets, dict) else None
|
|
534
|
+
|
|
535
|
+
alembic_ini = domain_dir / "alembic.ini"
|
|
536
|
+
env_py = domain_dir / "migrations" / "env.py"
|
|
537
|
+
versions_dir = domain_dir / "migrations" / "versions"
|
|
538
|
+
scaffold_requested = any(
|
|
539
|
+
[
|
|
540
|
+
alembic_ini.exists(),
|
|
541
|
+
env_py.exists(),
|
|
542
|
+
versions_dir.exists(),
|
|
543
|
+
isinstance(migrate_target, dict),
|
|
544
|
+
]
|
|
545
|
+
)
|
|
546
|
+
if not scaffold_requested:
|
|
547
|
+
return CheckResult(
|
|
548
|
+
group="Generated Packages & Apps",
|
|
549
|
+
area="Domain migration scaffold (optional)",
|
|
550
|
+
status=True,
|
|
551
|
+
path="not scaffolded",
|
|
552
|
+
suggestion=f"rpr generate domain {name} --migrations",
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
options = migrate_target.get("options", {}) if isinstance(migrate_target, dict) else {}
|
|
556
|
+
command_ok = isinstance(options, dict) and options.get("command") == (
|
|
557
|
+
"uv run alembic -c packages/python/domain/alembic.ini"
|
|
558
|
+
)
|
|
559
|
+
cwd_ok = isinstance(options, dict) and options.get("cwd") == "."
|
|
560
|
+
env_ok = False
|
|
561
|
+
if env_py.exists():
|
|
562
|
+
env_content = env_py.read_text()
|
|
563
|
+
env_ok = (
|
|
564
|
+
f"from {name_underscore}_domain.config.settings import Settings" in env_content
|
|
565
|
+
and f"import {name_underscore}_domain.entities" in env_content
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
missing: list[str] = []
|
|
569
|
+
if not alembic_ini.exists():
|
|
570
|
+
missing.append("alembic.ini")
|
|
571
|
+
if not env_py.exists():
|
|
572
|
+
missing.append("migrations/env.py")
|
|
573
|
+
if not versions_dir.is_dir():
|
|
574
|
+
missing.append("migrations/versions/")
|
|
575
|
+
if not isinstance(migrate_target, dict):
|
|
576
|
+
missing.append("migrate target")
|
|
577
|
+
elif not command_ok or not cwd_ok:
|
|
578
|
+
missing.append("migrate target command")
|
|
579
|
+
if env_py.exists() and not env_ok:
|
|
580
|
+
missing.append("env.py imports")
|
|
581
|
+
|
|
582
|
+
return CheckResult(
|
|
583
|
+
group="Generated Packages & Apps",
|
|
584
|
+
area="Domain migration scaffold (optional)",
|
|
585
|
+
status=not missing,
|
|
586
|
+
path=str(domain_dir)
|
|
587
|
+
if not missing
|
|
588
|
+
else f"{domain_dir} incomplete migration scaffold: {', '.join(missing)}",
|
|
589
|
+
suggestion=f"rpr generate domain {name} --migrations",
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _check_engine_install_metadata(
|
|
594
|
+
*, engine_dir: Path, suggestion: str
|
|
595
|
+
) -> CheckResult:
|
|
596
|
+
pyproject = engine_dir / "pyproject.toml"
|
|
597
|
+
data = _load_toml(pyproject)
|
|
598
|
+
if data is None:
|
|
599
|
+
return CheckResult(
|
|
600
|
+
group="Generated Packages & Apps",
|
|
601
|
+
area="Engine install metadata",
|
|
602
|
+
status=False,
|
|
603
|
+
path=f"{pyproject} missing or invalid",
|
|
604
|
+
suggestion=suggestion,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
build_system = data.get("build-system", {})
|
|
608
|
+
requires = build_system.get("requires", []) if isinstance(build_system, dict) else []
|
|
609
|
+
requires_set = set(requires) if isinstance(requires, list) else set()
|
|
610
|
+
status = "maturin>=1.8.2,<2.0" in requires_set
|
|
611
|
+
return CheckResult(
|
|
612
|
+
group="Generated Packages & Apps",
|
|
613
|
+
area="Engine install metadata",
|
|
614
|
+
status=status,
|
|
615
|
+
path=str(pyproject)
|
|
616
|
+
if status
|
|
617
|
+
else f"{pyproject} missing maturin build requirement",
|
|
618
|
+
suggestion=suggestion,
|
|
619
|
+
)
|