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/map/walker.py ADDED
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ import pathspec
8
+
9
+ type Language = Literal["python", "typescript", "javascript", "rust", "cpp"]
10
+
11
+ _EXT_TO_LANG: dict[str, Language] = {
12
+ ".py": "python",
13
+ ".ts": "typescript",
14
+ ".tsx": "typescript",
15
+ ".js": "javascript",
16
+ ".jsx": "javascript",
17
+ ".mjs": "javascript",
18
+ ".cjs": "javascript",
19
+ ".rs": "rust",
20
+ ".cpp": "cpp",
21
+ ".cc": "cpp",
22
+ ".cxx": "cpp",
23
+ ".c": "cpp",
24
+ ".h": "cpp",
25
+ ".hpp": "cpp",
26
+ ".hxx": "cpp",
27
+ }
28
+
29
+ _IGNORED_DIRS: frozenset[str] = frozenset(
30
+ {
31
+ ".venv",
32
+ "venv",
33
+ ".env",
34
+ "node_modules",
35
+ "__pycache__",
36
+ "dist",
37
+ "build",
38
+ ".nx",
39
+ "target",
40
+ ".git",
41
+ "vendor",
42
+ }
43
+ )
44
+
45
+
46
+ def _load_gitignore(root: Path) -> pathspec.PathSpec | None:
47
+ """Load .gitignore patterns from root; returns None if file absent."""
48
+ gitignore = root / ".gitignore"
49
+ if not gitignore.is_file():
50
+ return None
51
+ lines = gitignore.read_text(encoding="utf-8", errors="replace").splitlines()
52
+ return pathspec.PathSpec.from_lines("gitignore", lines)
53
+
54
+
55
+ @dataclass
56
+ class SourceFile:
57
+ abs_path: Path
58
+ rel_path: Path
59
+ language: Language
60
+
61
+
62
+ @dataclass
63
+ class ProjectEntry:
64
+ abs_path: Path
65
+ rel_path: Path
66
+ is_dir: bool
67
+
68
+
69
+ def walk(root: Path) -> list[SourceFile]:
70
+ spec = _load_gitignore(root)
71
+ results: list[SourceFile] = []
72
+ _recurse(root, root, results, spec)
73
+ return results
74
+
75
+
76
+ def list_project_entries(root: Path) -> list[ProjectEntry]:
77
+ spec = _load_gitignore(root)
78
+ results: list[ProjectEntry] = []
79
+ _recurse_entries(root, root, results, spec)
80
+ return results
81
+
82
+
83
+ def _recurse(
84
+ root: Path,
85
+ current: Path,
86
+ results: list[SourceFile],
87
+ spec: pathspec.PathSpec | None,
88
+ ) -> None:
89
+ try:
90
+ entries = list(current.iterdir())
91
+ except PermissionError:
92
+ return
93
+ for entry in entries:
94
+ if entry.is_symlink():
95
+ continue
96
+ rel = entry.relative_to(root)
97
+ if entry.is_dir():
98
+ if entry.name in _IGNORED_DIRS:
99
+ continue
100
+ # Append "/" so pathspec matches directory-only patterns (e.g. "secret/")
101
+ if spec is not None and spec.match_file(str(rel) + "/"):
102
+ continue
103
+ _recurse(root, entry, results, spec)
104
+ elif entry.is_file():
105
+ if spec is not None and spec.match_file(str(rel)):
106
+ continue
107
+ lang = _EXT_TO_LANG.get(entry.suffix)
108
+ if lang is not None:
109
+ results.append(
110
+ SourceFile(
111
+ abs_path=entry.resolve(),
112
+ rel_path=rel,
113
+ language=lang,
114
+ )
115
+ )
116
+
117
+
118
+ def _recurse_entries(
119
+ root: Path,
120
+ current: Path,
121
+ results: list[ProjectEntry],
122
+ spec: pathspec.PathSpec | None,
123
+ ) -> None:
124
+ try:
125
+ entries = sorted(
126
+ current.iterdir(),
127
+ key=lambda entry: (not entry.is_dir(), entry.name.lower()),
128
+ )
129
+ except PermissionError:
130
+ return
131
+
132
+ for entry in entries:
133
+ if entry.is_symlink():
134
+ continue
135
+ rel = entry.relative_to(root)
136
+ if entry.is_dir():
137
+ if entry.name in _IGNORED_DIRS:
138
+ continue
139
+ if spec is not None and spec.match_file(str(rel) + "/"):
140
+ continue
141
+ results.append(
142
+ ProjectEntry(abs_path=entry.resolve(), rel_path=rel, is_dir=True)
143
+ )
144
+ _recurse_entries(root, entry, results, spec)
145
+ continue
146
+
147
+ if spec is not None and spec.match_file(str(rel)):
148
+ continue
149
+ results.append(
150
+ ProjectEntry(abs_path=entry.resolve(), rel_path=rel, is_dir=False)
151
+ )
@@ -0,0 +1,30 @@
1
+ Reference scaffold for the shared SQLModel base entity.
2
+
3
+ Uses SQLAlchemy server-side defaults so that `id`, `created_at`, and `updated_at`
4
+ are generated by the database rather than the Python runtime.
5
+
6
+ ```python
7
+ from datetime import datetime
8
+
9
+ from sqlalchemy import DateTime, String, func
10
+ from sqlmodel import Field, SQLModel
11
+
12
+
13
+ class BaseEntity(SQLModel):
14
+ id: str = Field(
15
+ default=None,
16
+ primary_key=True,
17
+ sa_type=String(36),
18
+ sa_column_kwargs={"server_default": func.gen_random_uuid()},
19
+ )
20
+ created_at: datetime = Field(
21
+ default=None,
22
+ sa_type=DateTime(timezone=True),
23
+ sa_column_kwargs={"server_default": func.now()},
24
+ )
25
+ updated_at: datetime = Field(
26
+ default=None,
27
+ sa_type=DateTime(timezone=True),
28
+ sa_column_kwargs={"server_default": func.now(), "onupdate": func.now()},
29
+ )
30
+ ```
@@ -0,0 +1,48 @@
1
+ Reference scaffold for the shared async base repository.
2
+
3
+ Uses `AsyncSession` from `sqlmodel.ext.asyncio.session` and the `@mapper`
4
+ decorator from `utils/mapper_util` to handle DTO conversion transparently.
5
+ Replace `your_app_domain` with the actual package name during code generation.
6
+
7
+ ```python
8
+ from abc import ABC
9
+ from collections.abc import Callable
10
+ from typing import Generic, TypeVar
11
+
12
+ from sqlalchemy.ext.asyncio import AsyncEngine
13
+ from sqlmodel import SQLModel, select
14
+ from sqlmodel.ext.asyncio.session import AsyncSession
15
+
16
+ from your_app_domain.utils.mapper_util import mapper
17
+
18
+ ModelType = TypeVar("ModelType", bound=SQLModel)
19
+
20
+
21
+ class BaseRepository(Generic[ModelType], ABC):
22
+ def __init__(
23
+ self,
24
+ model: type[ModelType],
25
+ engine: AsyncEngine,
26
+ dto_func: Callable,
27
+ ):
28
+ self.model = model
29
+ self.engine = engine
30
+ self.dto_func = dto_func
31
+
32
+ @mapper
33
+ async def get_by_id(self, id: str) -> ModelType | None:
34
+ async with AsyncSession(self.engine) as session:
35
+ statement = select(self.model).where(self.model.id == id)
36
+ result = await session.exec(statement)
37
+ return result.first()
38
+
39
+ @mapper
40
+ async def create(self, obj_in: ModelType) -> ModelType:
41
+ async with AsyncSession(self.engine) as session:
42
+ obj_data = obj_in.model_dump()
43
+ db_obj = self.model(**obj_data)
44
+ session.add(db_obj)
45
+ await session.commit()
46
+ await session.refresh(db_obj)
47
+ return db_obj
48
+ ```
@@ -0,0 +1,76 @@
1
+ ## Domain Container Template
2
+
3
+ Target path: `packages/python/domain/src/{name_underscore}_domain/config/container.py`
4
+
5
+ Use this template when scaffolding the domain DI container. It wires the core infrastructure providers — database engine and HTTP client — as singletons driven by `Settings`.
6
+
7
+ The first Python block is the generated file used by `rpr generate domain` and `rpr add template domain_container`.
8
+
9
+ ```python
10
+ from __future__ import annotations
11
+
12
+ import httpx
13
+ from dependency_injector import containers, providers
14
+ from sqlalchemy.ext.asyncio import create_async_engine
15
+
16
+ from your_app_domain.config.settings import Settings
17
+
18
+
19
+ class Container(containers.DeclarativeContainer):
20
+ wiring_config = containers.WiringConfiguration(modules=[])
21
+
22
+ config = providers.Singleton(Settings)
23
+
24
+ db_engine = providers.Singleton(
25
+ lambda settings: create_async_engine(
26
+ settings.database_url,
27
+ echo=settings.db_echo,
28
+ pool_pre_ping=True,
29
+ pool_recycle=3600,
30
+ ),
31
+ settings=config,
32
+ )
33
+
34
+ http_client = providers.Singleton(
35
+ httpx.AsyncClient,
36
+ timeout=config.provided.http_timeout,
37
+ limits=providers.Factory(
38
+ httpx.Limits,
39
+ max_connections=config.provided.httpx_max_connections,
40
+ max_keepalive_connections=config.provided.http_max_keepalive,
41
+ ),
42
+ )
43
+
44
+ # To add lifecycle methods (startup/shutdown hooks), override __new__:
45
+ #
46
+ # def __new__(cls):
47
+ # instance = super().__new__(cls)
48
+ # return add_lifecycle_methods(instance)
49
+ #
50
+ # Implement add_lifecycle_methods() in this module to attach
51
+ # startup and shutdown handlers to the container instance.
52
+ ```
53
+
54
+ ## What This Template Provides
55
+
56
+ - `config`: a singleton `Settings` instance that reads from `.env`.
57
+ - `db_engine`: a singleton async SQLAlchemy engine built from `settings.database_url`, with connection-pool tuning defaults (`pool_pre_ping`, `pool_recycle`).
58
+ - `http_client`: a singleton `httpx.AsyncClient` with timeout and connection limits sourced from `Settings`.
59
+
60
+ ## Lifecycle Extension Pattern
61
+
62
+ If the project needs startup or shutdown hooks (e.g., acquiring a connection pool, registering signal handlers), override `__new__` on `Container`:
63
+
64
+ ```python
65
+ def __new__(cls):
66
+ instance = super().__new__(cls)
67
+ return add_lifecycle_methods(instance)
68
+ ```
69
+
70
+ Define `add_lifecycle_methods(instance)` in the same module to attach `on_startup` and `on_shutdown` handlers without modifying the provider declarations.
71
+
72
+ ## Usage Notes
73
+
74
+ - Register new services and repositories as providers directly on `Container`; do not subclass it.
75
+ - Inject the container into FastAPI (or the framework of your choice) via its `wire()` method in the app factory.
76
+ - Keep transport-specific clients (e.g., third-party API clients) as additional `providers.Singleton` entries, bound to `config` for their credentials.
@@ -0,0 +1,57 @@
1
+ ## Domain Settings Template
2
+
3
+ Target path: `packages/python/domain/src/{name_underscore}_domain/config/settings.py`
4
+
5
+ Use this template when scaffolding the domain configuration layer. It provides a Pydantic `BaseSettings` class that loads from `.env` and defines all core infrastructure settings needed by the DI container.
6
+
7
+ The first Python block is the generated file used by `rpr generate domain` and `rpr add template domain_settings`.
8
+
9
+ ```python
10
+ from __future__ import annotations
11
+
12
+ from pydantic import Field
13
+ from pydantic_settings import BaseSettings, SettingsConfigDict
14
+
15
+
16
+ class Settings(BaseSettings):
17
+ model_config = SettingsConfigDict(
18
+ env_file=".env",
19
+ env_file_encoding="utf-8",
20
+ case_sensitive=False,
21
+ extra="ignore",
22
+ )
23
+
24
+ db_host: str = Field(default="localhost", description="Database host")
25
+ db_port: int = Field(default=5432, description="Database port")
26
+ db_name: str = Field(default="postgres", description="Database name")
27
+ db_user: str = Field(default="postgres", description="Database user")
28
+ db_password: str = Field(default="", description="Database password")
29
+ db_sslmode: str = Field(default="prefer", description="Database SSL mode")
30
+ db_echo: bool = Field(default=False, description="Enable SQL query logging")
31
+ db_encrypt_key: bytes | None = Field(default=None, description="Fernet encryption key")
32
+
33
+ http_timeout: float = Field(default=30.0, description="HTTP client timeout in seconds")
34
+ httpx_max_connections: int = Field(default=100, description="Maximum HTTP connections")
35
+ http_max_keepalive: int = Field(default=20, description="Maximum keepalive HTTP connections")
36
+
37
+ @property
38
+ def database_url(self) -> str:
39
+ return (
40
+ f"postgresql+asyncpg://{self.db_user}:{self.db_password}"
41
+ f"@{self.db_host}:{self.db_port}/{self.db_name}"
42
+ f"?sslmode={self.db_sslmode}"
43
+ )
44
+ ```
45
+
46
+ ## What This Template Provides
47
+
48
+ - Reads all settings from `.env` with `case_sensitive=False` and `extra="ignore"` so unknown keys never raise errors.
49
+ - Defines the full set of database fields used to build `database_url` in the `@property`.
50
+ - Defines `http_timeout`, `httpx_max_connections`, and `http_max_keepalive` so the container's `http_client` provider can bind them without extra lookups.
51
+ - `db_encrypt_key` is `bytes | None` to pair with the `encrypted_column` utility when needed.
52
+
53
+ ## Usage Notes
54
+
55
+ - Add corresponding keys to `.env.example` for each field (see `special_files.md` ENV section).
56
+ - Extend `Settings` in the domain package to add app-specific fields; do not modify this base set.
57
+ - `database_url` is a computed property — it is not read from `.env` directly.
@@ -0,0 +1,50 @@
1
+ ---
2
+ applyTo: "**"
3
+ description: "Project overview, monorepo structure, and tech stack for DDD architecture"
4
+ ---
5
+
6
+ # Project Overview
7
+
8
+ This project uses an NX + UV monorepo with a domain-driven design approach. Applications stay thin and delegate business logic to shared packages.
9
+
10
+ ## Architecture
11
+
12
+ - **Applications** (`apps/*`): frontend apps, APIs, and other entry points. Keep them focused on transport, composition, and configuration.
13
+ - **Packages**: shared domain logic, reusable UI code, Storybook, and Rust-backed engine code.
14
+ - **Domain layer**: entities, DTOs, repositories, services, workflows, and dependency injection. Keep boundaries explicit and data flow predictable.
15
+
16
+ ## Tech Stack
17
+
18
+ - NX for workspace orchestration
19
+ - UV for Python environments and dependency management
20
+ - React, TanStack Query, TanStack Router, and MUI on the Node/UI side
21
+ - FastAPI, SQLModel, Alembic, and Dependency Injector on the Python side
22
+ - Rust with PyO3 and Maturin for the engine package
23
+ - Storybook for isolated UI development
24
+
25
+ ## Layout
26
+
27
+ | Path | Purpose |
28
+ | ------------------------------- | ------------------------------------------------------------------------ |
29
+ | `apps/{name}-web` | Thin frontend application shell |
30
+ | `apps/{name}-api` | FastAPI application |
31
+ | `packages/node/{name}-ui` | Reusable UI package with components, hooks, services, and state |
32
+ | `packages/node/sb` | Storybook package |
33
+ | `packages/python/domain` | Python domain package containing the `src/{name_underscore}_domain` code |
34
+ | `packages/python/{name}-engine` | Rust engine package exposed to Python |
35
+
36
+ ## Instructions, Templates, and Scaffolds
37
+
38
+ - Instruction files describe when to use a pattern and what constraints to follow.
39
+ - `setup-guide.instructions.md` captures the NX + UV workspace contract for root setup, project layout, naming, and expected command surfaces.
40
+ - `tooling.instructions.md` defines workspace-wide linting, formatting, and pre-commit conventions.
41
+ - `tooling-setup.instructions.md` contains detailed setup guidance for root tooling files such as `pyproject.toml`, `nx.json`, `.pre-commit-config.yaml`, and `eslint.config.mjs`.
42
+ - Shared utility implementations should live in stable project paths so code can reuse them instead of duplicating the same helpers in multiple places.
43
+ - Files in `.github/scaffolds/` are reference examples. Use them as guidance if they exist, but do not force the codebase to match them when local conventions already differ.
44
+
45
+ ## Path-Specific Instructions
46
+
47
+ - **API**: `api.instructions.md` for routes, dependency injection, and error handling
48
+ - **Domain**: `domain.instructions.md` for entities, DTOs, repositories, services, and workflows
49
+ - **Frontend**: `frontend.instructions.md` for React, TanStack Query, TanStack Router, and MUI patterns
50
+ - **Rust engine**: `rust-engine.instructions.md` for PyO3, Maturin, and NX engine targets
@@ -0,0 +1,42 @@
1
+ ---
2
+ applyTo: "apps/*-api/**"
3
+ description: "FastAPI route patterns, dependency injection, and CRUD conventions"
4
+ ---
5
+
6
+ # API Conventions
7
+
8
+ Framework: FastAPI.
9
+
10
+ When creating or modifying `main.py`, use `.github/scaffolds/api/main.py` as a reference if it exists. Follow the same overall shape: app factory, lifespan wiring, middleware registration, and exception handlers.
11
+
12
+ ## Routes
13
+
14
+ - Keep route handlers thin: validate input, delegate to workflows or services, and translate the result into the response.
15
+ - Prefer one `APIRouter()` per route module.
16
+ - Set `response_model` on every public route.
17
+ - For typed parameters, prefer `Annotated[...]` with FastAPI helpers such as `Path`, `Query`, and `Body`.
18
+ - Request and response DTOs should use the project's JSON-model base so Python field names stay idiomatic while serialized JSON stays consistent.
19
+
20
+ ## Dependency Injection
21
+
22
+ - Use `@inject` with `Depends(Provide[Container.<service_name>])` for services resolved from the container.
23
+ - Do not instantiate repositories or domain services directly in route handlers.
24
+ - Wire modules during startup or lifespan setup, not lazily inside request handlers.
25
+
26
+ ## Error Handling
27
+
28
+ - Use `HTTPException(404)` for missing resources and other explicit `HTTPException` responses for expected client-facing failures.
29
+ - Log unexpected failures and convert them to the project's standard server-error response path.
30
+ - Do not use bare `except`; catch specific exceptions and keep error translation close to the boundary.
31
+
32
+ ## CRUD
33
+
34
+ - Check existence before update or delete operations.
35
+ - Use the correct HTTP status codes: `201` for create, `200` for successful reads or updates, and `204` for deletes with no body.
36
+ - Return DTOs from route handlers, never ORM entities.
37
+ - For partial updates, use a DTO with optional fields and apply the changes through `apply_partial_update` rather than hand-written field assignment.
38
+
39
+ ## Boundaries
40
+
41
+ - Route modules own HTTP concerns. They should not contain business workflows, persistence logic, or cross-service orchestration.
42
+ - Keep response shaping close to the route layer and business rules in the domain layer.
@@ -0,0 +1,93 @@
1
+ ---
2
+ applyTo: "packages/python/domain/**"
3
+ description: "Domain layer structure, entities, DTOs, repos, and workflows"
4
+ ---
5
+
6
+ # Domain Layer
7
+
8
+ Use a DDD-style domain layer with explicit boundaries, SQLModel entities, Dependency Injector for composition, and Alembic for schema changes.
9
+
10
+ ## Structure
11
+
12
+ - **entities**: SQLModel-based persistence models. Use the shared base entity pattern for ids and timestamps.
13
+ - **dtos**: request, response, and integration models.
14
+ - **repos**: data-access layer. Extend the shared base repository pattern.
15
+ - **services**: external clients and provider integrations. Keep transport-specific logic here, not in workflows.
16
+ - **workflows**: business use cases and multi-step orchestration. Pub/sub handlers belong in `workflows/handlers` when the project uses them.
17
+ - **config**: settings, dependency-injection container, URLs, and logging.
18
+
19
+ ## Utilities
20
+
21
+ - Keep shared domain utilities in `src/{name_underscore}_domain/utils/`.
22
+ - Use `apply_partial_update()` from `utils/partial_update.py` for PATCH-style entity updates instead of handwritten field assignment.
23
+ - Use `Mapper` and the `mapper` decorator from `utils/mapper_util.py` when repository methods should support both raw ORM access and `.to_dto()` conversion.
24
+ - Use `DtoHelper` and `dto_helper` from `utils/dto_util.py` when the same DTO mapper should support both single models and lists.
25
+ - Use `EncryptedColumn` from `utils/encrypted_column.py` for encrypted persisted values.
26
+
27
+ ## Reference Scaffolds
28
+
29
+ When creating or modifying shared domain primitives, use the matching file in `.github/scaffolds/domain/` as a reference implementation if it exists.
30
+
31
+ - `BaseEntity`: use `.github/scaffolds/domain/base_entity.md` as a reference for ids, timestamps, and SQLModel defaults.
32
+ - Base repository pattern: use `.github/scaffolds/domain/base-repo.md` as a reference for shared repository behavior.
33
+
34
+ These scaffold files are examples, not strict requirements. Prefer existing project-local conventions if the codebase already differs.
35
+
36
+ ## Migrations
37
+
38
+ - Do not create Alembic migration files by hand.
39
+ - Prefer the workspace migration command so schema changes run through the same project wiring as the rest of the repo.
40
+ - The standard target shape is an Nx `migrate` target on the domain project that executes `uv run alembic -c packages/python/domain/alembic.ini` from the workspace root.
41
+ - Run migration commands from the workspace root and keep the standalone `--` separator between Nx arguments and Alembic arguments.
42
+
43
+ ```sh
44
+ npx nx run domain:migrate -- revision --autogenerate -m "{number}_a_descriptive_message"
45
+ npx nx run domain:migrate -- upgrade head
46
+ npx nx run domain:migrate -- downgrade -1
47
+ ```
48
+
49
+ - Keep the Alembic CLI under the project's configured Python environment.
50
+ - Alembic autogeneration depends on model discovery, so make sure new entity modules are imported by the domain model registry such as `entities/__init__.py`.
51
+
52
+ ## Repository Pattern
53
+
54
+ - Repositories should extend the project's shared base repository abstraction. Do not introduce competing base repository names or patterns in the same codebase.
55
+ - The base constructor should receive the model type, engine, and default DTO mapper used by the repository.
56
+ - Use the `@mapper` decorator when a repository method should support `.to_dto()` in addition to returning the raw ORM result.
57
+ - Use `@mapper(dto_func=...)` only when a method needs a mapper different from the repository default.
58
+ - For session management, prefer `async with AsyncSession(self.engine) as session`.
59
+ - Methods suffixed with `_in_session` should work inside an existing session and should not commit on their own.
60
+
61
+ ## Entity Pattern
62
+
63
+ - Entities inherit from `BaseEntity`.
64
+ - Prefer server-side defaults where the database should own the value.
65
+ - Always set `__tablename__` to the table name in the database.
66
+ - Use `__table_args__` to define table-level concerns such as indexes, constraints, and schema.
67
+ - Prefer SQLModel APIs. Use raw SQLAlchemy only when SQLModel does not expose what you need, such as `func.now()` or `func.gen_random_uuid()` for database-level defaults.
68
+
69
+ ## Service Pattern
70
+
71
+ - Register new services in the DI `Container`.
72
+ - Dependencies are injected through constructors, not ad hoc method parameters.
73
+ - Use `get_logger(self.__class__.__name__)` for service-local logging.
74
+ - Keep workflows responsible for multi-step business logic and coordination across services and repositories.
75
+
76
+ ## DTO Discipline
77
+
78
+ - Reuse existing DTOs when the contract is the same. Do not create near-duplicate DTOs for every small variation.
79
+ - DTOs exposed through the API should stay minimal and focused on the contract, not internal persistence details.
80
+ - Mapper functions should live in the DTO or mapper layer and follow a clear `to_dto()` naming convention.
81
+ - DTOs that are serialized to JSON should use the project's camelCase-aware base model.
82
+ - DTOs that represent external-service payloads can use `TypedDict` when a Pydantic model would add unnecessary weight.
83
+ - Internal data containers that are not serialized can use `@dataclass` when that is the simplest fit.
84
+
85
+ ## Log Injection Prevention
86
+
87
+ - Add a shared `sanitize_for_log()` helper if the project does not already have one.
88
+ - Sanitize untrusted values before logging them, especially identifiers and externally supplied strings.
89
+
90
+ ## Testing
91
+
92
+ - Service and repository tests should verify DTO mapping behavior, including `.to_dto()` paths when `mapper_util` is in use.
93
+ - Use `@pytest.fixture` for reusable test setup.
@@ -0,0 +1,97 @@
1
+ ---
2
+ applyTo: "packages/node/*-ui/**,apps/*/**"
3
+ description: "React components, TanStack Query/Router, MUI patterns"
4
+ ---
5
+
6
+ # Frontend (UI Library)
7
+
8
+ Use React for UI composition, TanStack Query for server-state workflows, TanStack Router for routing, and MUI for the component system.
9
+
10
+ ## Patterns
11
+
12
+ - Use functional components.
13
+ - Extract reusable behavior into hooks instead of duplicating component logic.
14
+ - Prefer a shared base API service at `src/services/base/api.service.ts` when the UI package needs centralized fetch behavior.
15
+ - Prefer a shared `useStateNavigation` hook at `src/hooks/useStateNavigation.ts` when UI state should persist in the URL.
16
+ - Reuse existing components, hooks, and service patterns before introducing a new abstraction.
17
+ - Use TanStack Query for server state and request lifecycles.
18
+ - Organize API access per domain: service methods in `services/`, query hooks near stateful consumers or in shared hooks when reused broadly.
19
+ - On successful mutations, update or invalidate the relevant query cache instead of forcing a full-page reload.
20
+
21
+ ## Reference Scaffolds
22
+
23
+ - When creating or modifying the shared base API service at `src/services/base/api.service.ts`, use `.github/scaffolds/frontend/api.service.ts` as a reference if it exists.
24
+ - When creating or modifying the shared `useStateNavigation` hook at `src/hooks/useStateNavigation.ts`, use `.github/scaffolds/frontend/useStateNavigation.ts` as a reference if it exists.
25
+ - These scaffold files are examples, not strict requirements. Prefer existing project-local conventions if the UI package already differs.
26
+
27
+ Example mutation pattern:
28
+
29
+ ```ts
30
+ type User = {
31
+ id: string;
32
+ name: string;
33
+ };
34
+
35
+ export const useChangeName = (id: string, newName: string) => {
36
+ const queryClient = useQueryClient();
37
+
38
+ return useMutation({
39
+ mutationFn: () => userService.changeName(id, newName),
40
+ onSuccess: () => {
41
+ queryClient.setQueryData<User | undefined>(["user", id], (oldData) => {
42
+ if (!oldData) {
43
+ return oldData;
44
+ }
45
+
46
+ return {
47
+ ...oldData,
48
+ name: newName,
49
+ };
50
+ });
51
+ },
52
+ });
53
+ };
54
+ ```
55
+
56
+ ## Routing
57
+
58
+ - Use TanStack Router for type-safe route definitions.
59
+ - Keep route configuration close to the app boundary. The UI library should not own application-only route composition.
60
+ - Prefer explicit route trees built from `createRootRoute`, `createRoute`, `addChildren`, and `createRouter`.
61
+
62
+ ## Styling
63
+
64
+ - If a component needs non-trivial custom styling, place it in a sibling `{Component}.styles.tsx` file.
65
+ - Use Emotion through MUI's `styled` API and prefer template-literal or callback-based styled components over scattered inline style objects.
66
+
67
+ Example:
68
+
69
+ ```ts
70
+ export const SomeContainer = styled(Box)`
71
+ display: flex;
72
+ `;
73
+ ```
74
+
75
+ - Prefer theme-driven colors and tokens so light and dark mode stay consistent.
76
+ - Let `Paper` and `Typography` carry default background and text colors unless a component has a specific design reason to override them.
77
+ - Create and maintain a shared app theme instead of scattering palette decisions across components.
78
+ - Use the `sx` prop sparingly. It is appropriate for one-off local adjustments or truly dynamic runtime values. Shared or repeated styles should move into the theme or a styled wrapper.
79
+ - Components should not hard-code their own layout width unless the component's contract requires it. Let parent containers control sizing.
80
+ - Prefer flexbox or grid for layout rather than margin-based positioning.
81
+ - Extend MUI theme types through a declaration file in the UI package when custom tokens are required.
82
+
83
+ ## Typing
84
+
85
+ - Type every API payload and mutation input.
86
+ - Prefer `type` aliases for DTO-shaped data, `interface` for extendable service contracts, and classes only when the UI genuinely needs behavior.
87
+ - For components with children, prefer `React.PropsWithChildren<T>` over manually re-declaring a `children` field.
88
+
89
+ ## Components
90
+
91
+ - Use functional components.
92
+ - Use `useLayoutEffect` only when the effect must run before paint, such as DOM measurement or URL synchronization. Use `useEffect` by default for ordinary side effects.
93
+ - Keep components as small and focused as practical.
94
+ - Isolate side effects into hooks or boundary components. Query hooks are the normal exception because they are already side-effect boundaries.
95
+ - Prefer pure helpers and deterministic render logic where possible to keep testing straightforward.
96
+ - Do not memoize by default. Add memoization only when render churn is measurable or the component contract clearly benefits from it.
97
+ - Be deliberate about prop shape and state ownership to avoid unnecessary re-renders.