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.
Files changed (97) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/agent/__init__.py +29 -0
  3. rpr/agent/approval.py +19 -0
  4. rpr/agent/bootstrap.py +60 -0
  5. rpr/agent/client.py +115 -0
  6. rpr/agent/control.py +16 -0
  7. rpr/agent/mock.py +158 -0
  8. rpr/agent/runtime.py +291 -0
  9. rpr/agent/session.py +91 -0
  10. rpr/agent/tools/__init__.py +10 -0
  11. rpr/agent/tools/base.py +62 -0
  12. rpr/agent/tools/mutating.py +47 -0
  13. rpr/agent/tools/readonly.py +147 -0
  14. rpr/agent/tools/registry.py +30 -0
  15. rpr/application/__init__.py +1 -0
  16. rpr/application/catalog.py +427 -0
  17. rpr/application/chat_service.py +413 -0
  18. rpr/application/checks.py +49 -0
  19. rpr/application/cli_adapter.py +36 -0
  20. rpr/application/completer.py +163 -0
  21. rpr/application/conversation_service.py +92 -0
  22. rpr/application/prompt_service.py +85 -0
  23. rpr/application/selector.py +240 -0
  24. rpr/application/shell.py +543 -0
  25. rpr/checks/__init__.py +0 -0
  26. rpr/checks/base.py +13 -0
  27. rpr/checks/instructions.py +89 -0
  28. rpr/checks/packages.py +619 -0
  29. rpr/checks/workspace.py +170 -0
  30. rpr/cli.py +78 -0
  31. rpr/commands/__init__.py +0 -0
  32. rpr/commands/add.py +166 -0
  33. rpr/commands/chat.py +48 -0
  34. rpr/commands/check.py +53 -0
  35. rpr/commands/generate/__init__.py +0 -0
  36. rpr/commands/generate/api.py +228 -0
  37. rpr/commands/generate/domain.py +383 -0
  38. rpr/commands/generate/engine.py +148 -0
  39. rpr/commands/generate/storybook.py +442 -0
  40. rpr/commands/generate/ui.py +414 -0
  41. rpr/commands/init.py +822 -0
  42. rpr/commands/map.py +113 -0
  43. rpr/commands/settings.py +102 -0
  44. rpr/commands/sync.py +97 -0
  45. rpr/context.py +203 -0
  46. rpr/generators/__init__.py +0 -0
  47. rpr/generators/base.py +110 -0
  48. rpr/generators/claude.py +33 -0
  49. rpr/generators/copilot.py +36 -0
  50. rpr/generators/cursor.py +38 -0
  51. rpr/generators/gemini.py +33 -0
  52. rpr/map/__init__.py +0 -0
  53. rpr/map/architecture.py +495 -0
  54. rpr/map/chains.py +317 -0
  55. rpr/map/classifier.py +170 -0
  56. rpr/map/coverage.py +200 -0
  57. rpr/map/dependencies.py +243 -0
  58. rpr/map/extractor.py +223 -0
  59. rpr/map/graph.py +318 -0
  60. rpr/map/output.py +1030 -0
  61. rpr/map/responsibility.py +345 -0
  62. rpr/map/topology.py +327 -0
  63. rpr/map/walker.py +151 -0
  64. rpr/scaffolds/domain/base_entity.md +30 -0
  65. rpr/scaffolds/domain/base_repo.md +48 -0
  66. rpr/scaffolds/domain/container.md +76 -0
  67. rpr/scaffolds/domain/settings.md +57 -0
  68. rpr/scaffolds/instructions/all.instructions.md +50 -0
  69. rpr/scaffolds/instructions/api.instructions.md +42 -0
  70. rpr/scaffolds/instructions/domain.instructions.md +93 -0
  71. rpr/scaffolds/instructions/frontend.instructions.md +97 -0
  72. rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
  73. rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
  74. rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
  75. rpr/scaffolds/instructions/tooling.instructions.md +42 -0
  76. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  77. rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
  78. rpr/scaffolds/special_files/domain_container.md +76 -0
  79. rpr/scaffolds/special_files/domain_settings.md +57 -0
  80. rpr/scaffolds/special_files/dto_util.md +62 -0
  81. rpr/scaffolds/special_files/encrypted_column.md +98 -0
  82. rpr/scaffolds/special_files/mapper_util.md +159 -0
  83. rpr/scaffolds/special_files/partial_update.md +61 -0
  84. rpr/templates/__init__.py +0 -0
  85. rpr/templates/registry.py +81 -0
  86. rpr/ui/__init__.py +0 -0
  87. rpr/ui/console.py +32 -0
  88. rpr/ui/markdown.py +59 -0
  89. rpr/ui/prompt_session.py +430 -0
  90. rpr/ui/renderers.py +167 -0
  91. rpr/ui/theme.py +286 -0
  92. rpr/workspace.py +131 -0
  93. rpr_cli-0.1.1.dist-info/METADATA +201 -0
  94. rpr_cli-0.1.1.dist-info/RECORD +97 -0
  95. rpr_cli-0.1.1.dist-info/WHEEL +4 -0
  96. rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
  97. 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
+ )