From 4d3b9117b6750672865c1825936c9a310d365b8a Mon Sep 17 00:00:00 2001 From: intelligent04 Date: Tue, 4 Aug 2026 13:33:21 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=83=9D=EC=84=B1=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8D=98=20TEST=5FRULES.md=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0,=20=ED=94=84=EB=A0=88=EC=9E=84=EC=9B=8C=ED=81=AC=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=A4=91=EB=B3=B5=EA=B3=BC=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 세 가지를 정리한다. 1. TEST_RULES.md가 생성되지 않았다. _render_test_rules()와 프로필별 test_rules 데이터가 이미 있었지만 render()의 출력 목록에 연결되어 있지 않아 실제 생성물에는 테스트 규칙이 없었다. 출력에 추가하고 AGENTS.md 라우팅 표에도 넣었다. 2. 프레임워크별 규칙이 세 문서에 그대로 중복됐다. 동일한 문장이 GLOBAL_RULES.md, CODE_STYLE_RULES.md, SERVICE_LAYER_RULES.md에 세 번 들어갔다. 같은 문장을 반복해도 준수율은 오르지 않고 컨텍스트 예산만 쓴다. GLOBAL_RULES.md를 단일 출처로 두고 나머지 두 문서에는 참조 문구를 남긴다. 덤으로 한국어 SERVICE_LAYER_RULES.md에 영어 라벨(Service Notes)이 섞여 있던 문제도 사라진다. 3. 호출되지 않는 렌더러 메서드 5종(208줄)을 제거했다. 결과적으로 규칙 문서가 하나 늘었는데도 총량은 줄었다. fastapi: 8파일 21,824B -> 9파일 21,772B fastapi,react: 8파일 24,636B -> 9파일 24,192B - tests/test_rule_documents.py 4건 추가. 규칙 문서 간 중복 문장 0을 강제하고 생성된 모든 규칙 문서가 AGENTS.md 라우팅 표에서 도달 가능한지 검사한다. - 기존 테스트 중 SERVICE_LAYER_RULES.md에서 프로필명을 확인하던 단정은 단일 출처인 GLOBAL_RULES.md를 보도록 옮겼다. - CODE_STYLE_RULES.md의 공백 규칙은 유지보수자 의견에 따라 그대로 둔다. --- CHANGELOG.md | 10 + README.md | 5 +- codex_builder/template_renderer.py | 240 ++-------------------- tests/snapshots/default_ko.txt | 32 +-- tests/snapshots/fastapi_full_en.txt | 35 ++-- tests/snapshots/fastapi_react_full_ko.txt | 50 ++--- tests/snapshots/nextjs_light_ko.txt | 30 ++- tests/snapshots/python_standard_en.txt | 32 +-- tests/test_builder.py | 11 +- tests/test_cli.py | 4 +- tests/test_rule_documents.py | 78 +++++++ 11 files changed, 230 insertions(+), 297 deletions(-) create mode 100644 tests/test_rule_documents.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cf22a..902fb11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Added +- `TEST_RULES.md` 생성. 렌더러와 프로필별 테스트 규칙 데이터는 이미 있었지만 출력 목록에 + 연결되어 있지 않아 실제로는 생성되지 않았습니다. AGENTS.md 라우팅 표에도 추가했습니다. - 루트 `AGENTS.md` 진입점 생성. 코딩 에이전트가 자동으로 읽는 상위 지시문 경로를 확보하기 위한 변경입니다. `.codex/` 아래 문서는 자동 로드 대상이 아니므로 기존에는 `codex_start_prompt.txt`를 사용자가 직접 붙여넣지 않으면 생성된 규칙이 적용되지 않았습니다. 상세 규칙을 복사하지 않고 @@ -21,8 +23,16 @@ 스모크 실행, 별도 job에서 `python -m build` 및 `twine check`를 수행합니다. - 이 CHANGELOG. +### Changed + +- 프레임워크별 규칙을 `GLOBAL_RULES.md` 한 곳에서만 정의합니다. 기존에는 동일한 문장이 + `GLOBAL_RULES.md`, `CODE_STYLE_RULES.md`, `SERVICE_LAYER_RULES.md`에 그대로 세 번 + 들어갔습니다. 두 문서에는 참조 문구를 남깁니다. + ### Removed +- 호출되지 않던 렌더러 메서드 5종 제거(`_render_framework_rules`, `_render_project_overview`, + `_render_feature_spec`, `_render_api_spec`, `_render_db_spec`). 총 208줄. - 저장소에 커밋되어 있던 `release_0_3_0/` 빌드 산출물(`.whl`, `.tar.gz`). 배포 아티팩트는 버전 관리 대상이 아니며 GitHub Releases 또는 PyPI에서 받을 수 있습니다. diff --git a/README.md b/README.md index f847456..5f8c45a 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,8 @@ AGENTS.md │ ├── DOCUMENT_RULE.md │ ├── DOMAIN_MODEL_RULES.md │ ├── EXTERNAL_INTEGRATION_RULES.md -│ └── SERVICE_LAYER_RULES.md +│ ├── SERVICE_LAYER_RULES.md +│ └── TEST_RULES.md ├── ref_docs/ └── codex_start_prompt.txt @@ -198,6 +199,8 @@ docs/ 기존 `AGENTS.md`가 있으면 덮어쓰지 않으므로 직접 수정한 내용은 재생성 후에도 유지됩니다. 생성을 원하지 않으면 `--no-agents-md`를 사용하세요. +프레임워크별 규칙은 `GLOBAL_RULES.md`에만 두고 다른 문서에서는 참조합니다. 같은 문장을 여러 문서에 반복해도 준수율이 오르지는 않고 컨텍스트 예산만 소모하기 때문입니다. + `ai_rule_developer`는 코딩할 때 지켜야 하는 규칙입니다. `ref_docs`는 외부 아키텍처 문서, PRD, 리서치, 벤더 문서처럼 사용자가 임의로 추가하는 참고자료 공간이므로 디렉토리만 생성합니다. 프로젝트 자체 명세는 루트 `docs/` 아래에 생성되며, 기존 `docs/` 파일이 있으면 덮어쓰지 않습니다. `codex_start_prompt.txt`는 Codex가 작업 시작 전에 `.codex` 문서를 먼저 읽고 규칙 우선순위를 적용하도록 지시합니다. ## 지원 프레임워크 프로필 diff --git a/codex_builder/template_renderer.py b/codex_builder/template_renderer.py index 9babb5b..1789967 100644 --- a/codex_builder/template_renderer.py +++ b/codex_builder/template_renderer.py @@ -34,6 +34,7 @@ def render(self, config: ProjectConfig) -> dict[Path, str]: ai_rules / "DOMAIN_MODEL_RULES.md": self._render_domain_model_rules(config, profiles), ai_rules / "EXTERNAL_INTEGRATION_RULES.md": self._render_external_integration_rules(config, profiles), ai_rules / "SERVICE_LAYER_RULES.md": self._render_service_layer_rules(config, profiles), + ai_rules / "TEST_RULES.md": self._render_test_rules(config, profiles), docs / "architecture" / "directory.md": self._render_docs_directory(config, profiles), docs / "architecture" / "architecture.md": self._render_docs_architecture(config, profiles), docs / "architecture" / "component.md": self._render_docs_component(config, profiles), @@ -61,6 +62,19 @@ def render_directories(self, config: ProjectConfig) -> tuple[Path, ...]: docs / "database", ) + def _framework_rules_pointer(self, config: ProjectConfig) -> list[str]: + """Point at the single source of truth for framework-specific rules. + + The per-profile rule list used to be repeated verbatim in GLOBAL_RULES, + CODE_STYLE_RULES and SERVICE_LAYER_RULES. Repeating the same lines does + not make an agent follow them harder, it only spends context budget, so + they are kept in GLOBAL_RULES only and referenced from here. + """ + + if config.language == "en": + return ["", "[Framework Rules]", "- Framework-specific rules live in `GLOBAL_RULES.md`. Apply them together with this document."] + return ["", "[프레임워크 규칙]", "- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다."] + def _render_global_rules(self, config: ProjectConfig, profiles: tuple[FrameworkProfile, ...]) -> str: if config.language == "en": lines = [ @@ -315,9 +329,7 @@ def _render_code_style_rules(self, config: ProjectConfig, profiles: tuple[Framew "- Files that mix unrelated layers.", "- Formatting that fights the repository's configured formatter.", ] - for profile in profiles: - lines.extend(["", f"[{profile.display_name} Style Notes]"]) - lines.extend(self._bullets(profile.framework_rules(config.language))) + lines.extend(self._framework_rules_pointer(config)) return "\n".join(lines) lines = [ @@ -375,9 +387,7 @@ def _render_code_style_rules(self, config: ProjectConfig, profiles: tuple[Framew "- 서로 다른 계층을 섞은 파일", "- 저장소 formatter와 충돌하는 포맷팅", ] - for profile in profiles: - lines.extend(["", f"[{profile.display_name} 스타일 메모]"]) - lines.extend(self._bullets(profile.framework_rules(config.language))) + lines.extend(self._framework_rules_pointer(config)) return "\n".join(lines) def _render_api_design_rules(self, config: ProjectConfig, profiles: tuple[FrameworkProfile, ...]) -> str: @@ -864,9 +874,7 @@ def _render_service_layer_rules(self, config: ProjectConfig, profiles: tuple[Fra "- Mixing external API calls directly into unrelated business logic.", "- Cramming complex branching into one method.", ] - for profile in profiles: - lines.extend(["", f"[{profile.display_name} Service Notes]"]) - lines.extend(self._bullets(profile.framework_rules(config.language))) + lines.extend(self._framework_rules_pointer(config)) return "\n".join(lines) lines = [ @@ -898,9 +906,7 @@ def _render_service_layer_rules(self, config: ProjectConfig, profiles: tuple[Fra "- 외부 API 호출을 무관한 비즈니스 로직에 직접 섞기", "- 복잡한 분기 로직을 하나의 메서드에 몰아넣기", ] - for profile in profiles: - lines.extend(["", f"[{profile.display_name} Service Notes]"]) - lines.extend(self._bullets(profile.framework_rules(config.language))) + lines.extend(self._framework_rules_pointer(config)) return "\n".join(lines) def _render_test_rules(self, config: ProjectConfig, profiles: tuple[FrameworkProfile, ...]) -> str: @@ -934,18 +940,6 @@ def _render_test_rules(self, config: ProjectConfig, profiles: tuple[FrameworkPro lines.extend(["", "## DB 테스트", f"- `{config.database}` repository 동작과 migration/schema 가정을 검증한다."]) return "\n".join(lines) - def _render_framework_rules(self, config: ProjectConfig, profiles: tuple[FrameworkProfile, ...]) -> str: - if config.language == "en": - lines = [f"# Framework Rules - {config.project_name}", "", "Apply the rules for every active stack profile."] - else: - lines = [f"# 프레임워크 규칙 - {config.project_name}", "", "활성화된 stack profile별 규칙을 모두 적용한다."] - - for profile in profiles: - lines.extend(["", f"## {profile.display_name}", "", profile.philosophy(config.language)]) - lines.extend(self._bullets(profile.framework_rules(config.language))) - - return "\n".join(lines) - def _render_docs_directory(self, config: ProjectConfig, profiles: tuple[FrameworkProfile, ...]) -> str: if config.language == "en": lines = [ @@ -1348,202 +1342,6 @@ def _render_docs_database_schema(self, config: ProjectConfig) -> str: lines.extend(["", "## 인증 관련 테이블", "- users, roles, sessions, tokens, audit 관련 테이블이 있으면 문서화한다."]) return "\n".join(lines) - def _render_project_overview(self, config: ProjectConfig, profiles: tuple[FrameworkProfile, ...]) -> str: - stack_names = ", ".join(profile.display_name for profile in profiles) - database = config.database or ("Not specified" if config.language == "en" else "미지정") - auth = self._enabled_label(config.auth_enabled, config.language) - external_api = self._enabled_label(config.external_api_enabled, config.language) - - if config.language == "en": - return "\n".join( - [ - f"# Project Overview - {config.project_name}", - "", - "## Summary", - config.description or "Describe the project purpose, users, and core product value here.", - "", - "## Basic Information", - f"- Project name: {config.project_name}", - f"- Stack: {stack_names}", - f"- Database: {database}", - f"- Authentication: {auth}", - f"- External API integration: {external_api}", - f"- Documentation level: {config.docs_level}", - "", - "## Architecture Notes", - "- Keep this section updated with the actual directory structure and major module responsibilities.", - "- Record important decisions that affect implementation boundaries.", - "- Link detailed behavior to `FEATURE_SPEC.md`, API contracts to `API_SPEC.md`, and database details to `DB_SPEC.md`.", - ] - ) - - return "\n".join( - [ - f"# 프로젝트 개요 - {config.project_name}", - "", - "## 요약", - config.description or "프로젝트 목적, 사용자, 핵심 제품 가치를 여기에 작성한다.", - "", - "## 기본 정보", - f"- 프로젝트 이름: {config.project_name}", - f"- 스택: {stack_names}", - f"- 데이터베이스: {database}", - f"- 인증 사용: {auth}", - f"- 외부 API 연동: {external_api}", - f"- 문서화 수준: {config.docs_level}", - "", - "## 아키텍처 메모", - "- 실제 디렉토리 구조와 주요 모듈 책임을 최신 상태로 유지한다.", - "- 구현 경계에 영향을 주는 중요한 결정을 기록한다.", - "- 상세 기능은 `FEATURE_SPEC.md`, API 계약은 `API_SPEC.md`, DB 상세는 `DB_SPEC.md`에 연결한다.", - ] - ) - - def _render_feature_spec(self, config: ProjectConfig) -> str: - if config.language == "en": - lines = [ - f"# Feature Specification - {config.project_name}", - "", - "Use this document as the implementation reference for product behavior.", - "", - "## Feature List", - "| Feature | User | Behavior | Status | Notes |", - "| --- | --- | --- | --- | --- |", - "| Example feature | Example user | Describe expected behavior | planned | Replace this row |", - "", - "## User Flows", - "- Describe the main user flow step by step.", - "- Include validation, empty state, error state, and permission behavior.", - ] - if config.auth_enabled: - lines.extend(["", "## Auth Requirements", "- Define roles, protected actions, session/token behavior, and permission errors."]) - if config.external_api_enabled: - lines.extend(["", "## External Integration Requirements", "- Define upstream systems, input/output contracts, timeout behavior, and failure handling."]) - return "\n".join(lines) - - lines = [ - f"# 기능 명세 - {config.project_name}", - "", - "이 문서는 제품 동작 구현 시 참고하는 기능 명세다.", - "", - "## 기능 목록", - "| 기능 | 사용자 | 동작 | 상태 | 메모 |", - "| --- | --- | --- | --- | --- |", - "| 예시 기능 | 예시 사용자 | 기대 동작을 작성 | planned | 이 행을 교체 |", - "", - "## 사용자 흐름", - "- 주요 사용자 흐름을 단계별로 작성한다.", - "- validation, empty state, error state, permission behavior를 포함한다.", - ] - if config.auth_enabled: - lines.extend(["", "## 인증 요구사항", "- role, protected action, session/token 동작, permission error를 정의한다."]) - if config.external_api_enabled: - lines.extend(["", "## 외부 연동 요구사항", "- upstream system, input/output contract, timeout behavior, failure handling을 정의한다."]) - return "\n".join(lines) - - def _render_api_spec(self, config: ProjectConfig) -> str: - if config.language == "en": - lines = [ - f"# API Specification - {config.project_name}", - "", - "Document every public API contract here.", - "", - "## Endpoint Index", - "| Method | Path | Auth | Description |", - "| --- | --- | --- | --- |", - "| GET | /health | public | Health check |", - "", - "## Endpoint Detail Template", - "### METHOD /path", - "- Description:", - "- Auth:", - "- Path params:", - "- Query params:", - "- Request body:", - "- Response body:", - "- Status codes:", - "- Error cases:", - ] - if config.auth_enabled: - lines.extend(["", "## Auth Contract", "- Define token/session format, refresh rules, expiration, and required headers."]) - if config.external_api_enabled: - lines.extend(["", "## External API Mapping", "- Map internal endpoints to external upstream calls only when the contract is known."]) - return "\n".join(lines) - - lines = [ - f"# API 명세 - {config.project_name}", - "", - "공개 API 계약을 이 문서에 모두 기록한다.", - "", - "## 엔드포인트 목록", - "| Method | Path | Auth | 설명 |", - "| --- | --- | --- | --- |", - "| GET | /health | public | 상태 확인 |", - "", - "## 엔드포인트 상세 템플릿", - "### METHOD /path", - "- 설명:", - "- 인증:", - "- Path params:", - "- Query params:", - "- Request body:", - "- Response body:", - "- Status codes:", - "- Error cases:", - ] - if config.auth_enabled: - lines.extend(["", "## 인증 계약", "- token/session 형식, refresh 규칙, 만료, 필수 header를 정의한다."]) - if config.external_api_enabled: - lines.extend(["", "## 외부 API 매핑", "- 계약이 확인된 경우 내부 endpoint와 외부 upstream 호출 관계를 기록한다."]) - return "\n".join(lines) - - def _render_db_spec(self, config: ProjectConfig) -> str: - database = config.database or ("Not specified" if config.language == "en" else "미지정") - if config.language == "en": - lines = [ - f"# Database Specification - {config.project_name}", - "", - f"- Database: {database}", - "", - "## Table Template", - "### table_name", - "- Purpose:", - "- Owner module:", - "- Soft delete:", - "", - "| Column | Type | Required | Index | Description |", - "| --- | --- | --- | --- | --- |", - "| id | string/int | yes | primary | Primary identifier |", - "", - "## Relationship Template", - "- `table.column` -> `other_table.column`: describe cardinality and delete behavior.", - ] - if config.auth_enabled: - lines.extend(["", "## Auth Tables", "- Document users, roles, sessions, tokens, and audit-related tables when they exist."]) - return "\n".join(lines) - - lines = [ - f"# DB 명세 - {config.project_name}", - "", - f"- 데이터베이스: {database}", - "", - "## 테이블 템플릿", - "### table_name", - "- 목적:", - "- 담당 모듈:", - "- Soft delete:", - "", - "| Column | Type | Required | Index | 설명 |", - "| --- | --- | --- | --- | --- |", - "| id | string/int | yes | primary | 기본 식별자 |", - "", - "## 관계 템플릿", - "- `table.column` -> `other_table.column`: cardinality와 delete behavior를 설명한다.", - ] - if config.auth_enabled: - lines.extend(["", "## 인증 관련 테이블", "- users, roles, sessions, tokens, audit 관련 테이블이 있으면 문서화한다."]) - return "\n".join(lines) - def _render_agents_md(self, config: ProjectConfig) -> str: """Render the repository-root AGENTS.md entrypoint. @@ -1585,6 +1383,7 @@ def _render_agents_md(self, config: ProjectConfig) -> str: f"| Entity, schema, domain state | `{rule_dir}/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` |", f"| Use-case and business logic | `{rule_dir}/SERVICE_LAYER_RULES.md` |", f"| Third-party or upstream integration | `{rule_dir}/EXTERNAL_INTEGRATION_RULES.md` |", + f"| Tests | `{rule_dir}/TEST_RULES.md` |", f"| Documentation updates | `{rule_dir}/DOCUMENT_RULE.md` |", "", "## Precedence", @@ -1658,6 +1457,7 @@ def _render_agents_md(self, config: ProjectConfig) -> str: f"| Entity, schema, 도메인 상태 | `{rule_dir}/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` |", f"| 유스케이스와 비즈니스 로직 | `{rule_dir}/SERVICE_LAYER_RULES.md` |", f"| 외부 연동 | `{rule_dir}/EXTERNAL_INTEGRATION_RULES.md` |", + f"| 테스트 | `{rule_dir}/TEST_RULES.md` |", f"| 문서 갱신 | `{rule_dir}/DOCUMENT_RULE.md` |", "", "## 우선순위", diff --git a/tests/snapshots/default_ko.txt b/tests/snapshots/default_ko.txt index 617e293..a35648e 100644 --- a/tests/snapshots/default_ko.txt +++ b/tests/snapshots/default_ko.txt @@ -132,12 +132,8 @@ docs/database - 서로 다른 계층을 섞은 파일 - 저장소 formatter와 충돌하는 포맷팅 -[FastAPI 스타일 메모] -- APIRouter는 라우팅과 의존성 연결만 담당한다. -- Service가 FastAPI Response, Depends, Request에 직접 의존하지 않게 한다. -- Repository는 SQLAlchemy, SQLModel 등 실제 저장소 API를 감싸는 경계로 둔다. -- 예외는 도메인/서비스 예외로 먼저 표현하고 controller에서 HTTP 상태로 변환한다. -- 배경 작업, 외부 호출, 긴 작업은 service 하위의 명시적 adapter/client로 분리한다. +[프레임워크 규칙] +- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다. ======================================================================== # file: .codex/ai_rule_developer/DOCUMENT_RULE.md @@ -348,12 +344,23 @@ Service/Application 계층은 비즈니스 로직의 중심이다. - 외부 API 호출을 무관한 비즈니스 로직에 직접 섞기 - 복잡한 분기 로직을 하나의 메서드에 몰아넣기 -[FastAPI Service Notes] -- APIRouter는 라우팅과 의존성 연결만 담당한다. -- Service가 FastAPI Response, Depends, Request에 직접 의존하지 않게 한다. -- Repository는 SQLAlchemy, SQLModel 등 실제 저장소 API를 감싸는 경계로 둔다. -- 예외는 도메인/서비스 예외로 먼저 표현하고 controller에서 HTTP 상태로 변환한다. -- 배경 작업, 외부 호출, 긴 작업은 service 하위의 명시적 adapter/client로 분리한다. +[프레임워크 규칙] +- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다. + +======================================================================== +# file: .codex/ai_rule_developer/TEST_RULES.md +======================================================================== +# 테스트 규칙 - snapshot-default + +## 기본 규칙 +- 동작이 바뀌면 테스트를 추가하거나 수정한다. +- 성공 케이스, validation 실패, authorization 실패, 중요한 오류 경로를 검증한다. +- 구현 세부사항보다 공개 계약과 사용자 관찰 가능 동작을 기준으로 테스트한다. + +## FastAPI +- Service 테스트는 repository/client를 대체 객체로 격리한다. +- API 테스트는 TestClient 또는 httpx 기반으로 요청/응답 계약을 검증한다. +- Repository 테스트는 DB 스키마와 쿼리 동작을 별도로 검증한다. ======================================================================== # file: .codex/codex_start_prompt.txt @@ -431,6 +438,7 @@ Service/Application 계층은 비즈니스 로직의 중심이다. | Entity, schema, 도메인 상태 | `.codex/ai_rule_developer/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` | | 유스케이스와 비즈니스 로직 | `.codex/ai_rule_developer/SERVICE_LAYER_RULES.md` | | 외부 연동 | `.codex/ai_rule_developer/EXTERNAL_INTEGRATION_RULES.md` | +| 테스트 | `.codex/ai_rule_developer/TEST_RULES.md` | | 문서 갱신 | `.codex/ai_rule_developer/DOCUMENT_RULE.md` | ## 우선순위 diff --git a/tests/snapshots/fastapi_full_en.txt b/tests/snapshots/fastapi_full_en.txt index ef6b480..ff19001 100644 --- a/tests/snapshots/fastapi_full_en.txt +++ b/tests/snapshots/fastapi_full_en.txt @@ -157,12 +157,8 @@ Follow the existing formatter and linter first. When the formatter does not deci - Files that mix unrelated layers. - Formatting that fights the repository's configured formatter. -[FastAPI Style Notes] -- APIRouter should only handle routing and dependency wiring. -- Services should not directly depend on FastAPI Response, Depends, or Request. -- Repositories wrap storage APIs such as SQLAlchemy or SQLModel. -- Represent errors as domain/service exceptions first, then map them to HTTP statuses in controllers. -- Move background jobs, external calls, and long-running work into explicit adapters/clients below services. +[Framework Rules] +- Framework-specific rules live in `GLOBAL_RULES.md`. Apply them together with this document. ======================================================================== # file: .codex/ai_rule_developer/DOCUMENT_RULE.md @@ -386,12 +382,26 @@ The service/application layer is the center of business logic. - Mixing external API calls directly into unrelated business logic. - Cramming complex branching into one method. -[FastAPI Service Notes] -- APIRouter should only handle routing and dependency wiring. -- Services should not directly depend on FastAPI Response, Depends, or Request. -- Repositories wrap storage APIs such as SQLAlchemy or SQLModel. -- Represent errors as domain/service exceptions first, then map them to HTTP statuses in controllers. -- Move background jobs, external calls, and long-running work into explicit adapters/clients below services. +[Framework Rules] +- Framework-specific rules live in `GLOBAL_RULES.md`. Apply them together with this document. + +======================================================================== +# file: .codex/ai_rule_developer/TEST_RULES.md +======================================================================== +# Test Rules - snapshot-full-en + +## Base Rules +- Add or update tests when behavior changes. +- Cover success cases, validation failures, authorization failures, and important error paths. +- Keep tests aligned with public contracts rather than implementation trivia. + +## FastAPI +- Isolate service tests with repository/client doubles. +- Use TestClient or httpx-based API tests to verify request/response contracts. +- Verify repository tests against database schema and query behavior separately. + +## Database Tests +- Verify repository behavior and migration/schema assumptions for `postgres`. ======================================================================== # file: .codex/codex_start_prompt.txt @@ -468,6 +478,7 @@ Load only what the current task needs: | Entity, schema, domain state | `.codex/ai_rule_developer/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` | | Use-case and business logic | `.codex/ai_rule_developer/SERVICE_LAYER_RULES.md` | | Third-party or upstream integration | `.codex/ai_rule_developer/EXTERNAL_INTEGRATION_RULES.md` | +| Tests | `.codex/ai_rule_developer/TEST_RULES.md` | | Documentation updates | `.codex/ai_rule_developer/DOCUMENT_RULE.md` | ## Precedence diff --git a/tests/snapshots/fastapi_react_full_ko.txt b/tests/snapshots/fastapi_react_full_ko.txt index 0e747fe..31e8bff 100644 --- a/tests/snapshots/fastapi_react_full_ko.txt +++ b/tests/snapshots/fastapi_react_full_ko.txt @@ -178,18 +178,8 @@ docs/database - 서로 다른 계층을 섞은 파일 - 저장소 formatter와 충돌하는 포맷팅 -[FastAPI 스타일 메모] -- APIRouter는 라우팅과 의존성 연결만 담당한다. -- Service가 FastAPI Response, Depends, Request에 직접 의존하지 않게 한다. -- Repository는 SQLAlchemy, SQLModel 등 실제 저장소 API를 감싸는 경계로 둔다. -- 예외는 도메인/서비스 예외로 먼저 표현하고 controller에서 HTTP 상태로 변환한다. -- 배경 작업, 외부 호출, 긴 작업은 service 하위의 명시적 adapter/client로 분리한다. - -[React 스타일 메모] -- 컴포넌트 안에서 fetch/axios 호출을 직접 수행하지 않는다. -- 복잡한 상태 전이는 hook 또는 store action으로 분리한다. -- 공통 UI 컴포넌트와 기능 전용 컴포넌트를 분리한다. -- API 타입과 UI view model의 변환 위치를 명확히 둔다. +[프레임워크 규칙] +- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다. ======================================================================== # file: .codex/ai_rule_developer/DOCUMENT_RULE.md @@ -427,18 +417,31 @@ Service/Application 계층은 비즈니스 로직의 중심이다. - 외부 API 호출을 무관한 비즈니스 로직에 직접 섞기 - 복잡한 분기 로직을 하나의 메서드에 몰아넣기 -[FastAPI Service Notes] -- APIRouter는 라우팅과 의존성 연결만 담당한다. -- Service가 FastAPI Response, Depends, Request에 직접 의존하지 않게 한다. -- Repository는 SQLAlchemy, SQLModel 등 실제 저장소 API를 감싸는 경계로 둔다. -- 예외는 도메인/서비스 예외로 먼저 표현하고 controller에서 HTTP 상태로 변환한다. -- 배경 작업, 외부 호출, 긴 작업은 service 하위의 명시적 adapter/client로 분리한다. +[프레임워크 규칙] +- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다. -[React Service Notes] -- 컴포넌트 안에서 fetch/axios 호출을 직접 수행하지 않는다. -- 복잡한 상태 전이는 hook 또는 store action으로 분리한다. -- 공통 UI 컴포넌트와 기능 전용 컴포넌트를 분리한다. -- API 타입과 UI view model의 변환 위치를 명확히 둔다. +======================================================================== +# file: .codex/ai_rule_developer/TEST_RULES.md +======================================================================== +# 테스트 규칙 - snapshot-full + +## 기본 규칙 +- 동작이 바뀌면 테스트를 추가하거나 수정한다. +- 성공 케이스, validation 실패, authorization 실패, 중요한 오류 경로를 검증한다. +- 구현 세부사항보다 공개 계약과 사용자 관찰 가능 동작을 기준으로 테스트한다. + +## FastAPI +- Service 테스트는 repository/client를 대체 객체로 격리한다. +- API 테스트는 TestClient 또는 httpx 기반으로 요청/응답 계약을 검증한다. +- Repository 테스트는 DB 스키마와 쿼리 동작을 별도로 검증한다. + +## React +- 컴포넌트 테스트는 사용자 상호작용과 표시 결과를 검증한다. +- hook 테스트는 상태 전이와 비동기 흐름을 검증한다. +- service 테스트는 API client 경계와 오류 변환을 검증한다. + +## DB 테스트 +- `mysql` repository 동작과 migration/schema 가정을 검증한다. ======================================================================== # file: .codex/codex_start_prompt.txt @@ -518,6 +521,7 @@ Service/Application 계층은 비즈니스 로직의 중심이다. | Entity, schema, 도메인 상태 | `.codex/ai_rule_developer/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` | | 유스케이스와 비즈니스 로직 | `.codex/ai_rule_developer/SERVICE_LAYER_RULES.md` | | 외부 연동 | `.codex/ai_rule_developer/EXTERNAL_INTEGRATION_RULES.md` | +| 테스트 | `.codex/ai_rule_developer/TEST_RULES.md` | | 문서 갱신 | `.codex/ai_rule_developer/DOCUMENT_RULE.md` | ## 우선순위 diff --git a/tests/snapshots/nextjs_light_ko.txt b/tests/snapshots/nextjs_light_ko.txt index 1fc118b..c8da362 100644 --- a/tests/snapshots/nextjs_light_ko.txt +++ b/tests/snapshots/nextjs_light_ko.txt @@ -131,11 +131,8 @@ docs/database - 서로 다른 계층을 섞은 파일 - 저장소 formatter와 충돌하는 포맷팅 -[Next.js 스타일 메모] -- 'use client'는 필요한 파일에만 선언한다. -- 서버 전용 비밀값과 브라우저 공개 환경 변수를 구분한다. -- 데이터 변경은 server action 또는 route handler로 경계를 명확히 한다. -- 캐시, revalidate, dynamic 설정은 데이터 신선도 요구사항과 함께 문서화한다. +[프레임워크 규칙] +- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다. ======================================================================== # file: .codex/ai_rule_developer/DOCUMENT_RULE.md @@ -344,11 +341,23 @@ Service/Application 계층은 비즈니스 로직의 중심이다. - 외부 API 호출을 무관한 비즈니스 로직에 직접 섞기 - 복잡한 분기 로직을 하나의 메서드에 몰아넣기 -[Next.js Service Notes] -- 'use client'는 필요한 파일에만 선언한다. -- 서버 전용 비밀값과 브라우저 공개 환경 변수를 구분한다. -- 데이터 변경은 server action 또는 route handler로 경계를 명확히 한다. -- 캐시, revalidate, dynamic 설정은 데이터 신선도 요구사항과 함께 문서화한다. +[프레임워크 규칙] +- 프레임워크별 규칙은 `GLOBAL_RULES.md`에 있다. 이 문서와 함께 적용한다. + +======================================================================== +# file: .codex/ai_rule_developer/TEST_RULES.md +======================================================================== +# 테스트 규칙 - snapshot-next + +## 기본 규칙 +- 동작이 바뀌면 테스트를 추가하거나 수정한다. +- 성공 케이스, validation 실패, authorization 실패, 중요한 오류 경로를 검증한다. +- 구현 세부사항보다 공개 계약과 사용자 관찰 가능 동작을 기준으로 테스트한다. + +## Next.js +- 서버 로직은 service 단위 테스트로 우선 검증한다. +- 클라이언트 컴포넌트는 사용자 상호작용 중심으로 검증한다. +- 라우팅과 인증 흐름은 통합 테스트 또는 E2E 테스트로 검증한다. ======================================================================== # file: .codex/codex_start_prompt.txt @@ -426,6 +435,7 @@ Service/Application 계층은 비즈니스 로직의 중심이다. | Entity, schema, 도메인 상태 | `.codex/ai_rule_developer/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` | | 유스케이스와 비즈니스 로직 | `.codex/ai_rule_developer/SERVICE_LAYER_RULES.md` | | 외부 연동 | `.codex/ai_rule_developer/EXTERNAL_INTEGRATION_RULES.md` | +| 테스트 | `.codex/ai_rule_developer/TEST_RULES.md` | | 문서 갱신 | `.codex/ai_rule_developer/DOCUMENT_RULE.md` | ## 우선순위 diff --git a/tests/snapshots/python_standard_en.txt b/tests/snapshots/python_standard_en.txt index ab41a98..15067db 100644 --- a/tests/snapshots/python_standard_en.txt +++ b/tests/snapshots/python_standard_en.txt @@ -130,12 +130,8 @@ Follow the existing formatter and linter first. When the formatter does not deci - Files that mix unrelated layers. - Formatting that fights the repository's configured formatter. -[Python Style Notes] -- Avoid import-time side effects such as file creation, network calls, or process execution. -- Runnable modules should expose a `main()` entrypoint and keep direct execution under `if __name__ == "__main__"`. -- Do not add dependencies when the standard library is enough for the problem. -- Keep meaningful type hints on public functions and classes, and represent input/output models with explicit data structures. -- Separate pure computation from I/O so logic stays testable and reusable. +[Framework Rules] +- Framework-specific rules live in `GLOBAL_RULES.md`. Apply them together with this document. ======================================================================== # file: .codex/ai_rule_developer/DOCUMENT_RULE.md @@ -347,12 +343,23 @@ The service/application layer is the center of business logic. - Mixing external API calls directly into unrelated business logic. - Cramming complex branching into one method. -[Python Service Notes] -- Avoid import-time side effects such as file creation, network calls, or process execution. -- Runnable modules should expose a `main()` entrypoint and keep direct execution under `if __name__ == "__main__"`. -- Do not add dependencies when the standard library is enough for the problem. -- Keep meaningful type hints on public functions and classes, and represent input/output models with explicit data structures. -- Separate pure computation from I/O so logic stays testable and reusable. +[Framework Rules] +- Framework-specific rules live in `GLOBAL_RULES.md`. Apply them together with this document. + +======================================================================== +# file: .codex/ai_rule_developer/TEST_RULES.md +======================================================================== +# Test Rules - snapshot-python + +## Base Rules +- Add or update tests when behavior changes. +- Cover success cases, validation failures, authorization failures, and important error paths. +- Keep tests aligned with public contracts rather than implementation trivia. + +## Python +- Separate tests for pure logic from tests that cover filesystem, network, or environment I/O. +- Use `tmp_path`, monkeypatching, and test doubles to isolate filesystem and external dependencies. +- Lock down CLI arguments, configuration parsing, and error paths with regression tests. ======================================================================== # file: .codex/codex_start_prompt.txt @@ -427,6 +434,7 @@ Load only what the current task needs: | Entity, schema, domain state | `.codex/ai_rule_developer/DOMAIN_MODEL_RULES.md`, `docs/database/schema.md` | | Use-case and business logic | `.codex/ai_rule_developer/SERVICE_LAYER_RULES.md` | | Third-party or upstream integration | `.codex/ai_rule_developer/EXTERNAL_INTEGRATION_RULES.md` | +| Tests | `.codex/ai_rule_developer/TEST_RULES.md` | | Documentation updates | `.codex/ai_rule_developer/DOCUMENT_RULE.md` | ## Precedence diff --git a/tests/test_builder.py b/tests/test_builder.py index f120de0..e9cbdbd 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -14,6 +14,7 @@ def test_default_generation_success(tmp_path): assert result.codex_dir == tmp_path / ".codex" assert (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").exists() assert (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").exists() + assert (tmp_path / ".codex" / "ai_rule_developer" / "TEST_RULES.md").exists() assert (tmp_path / ".codex" / "ai_rule_developer" / "DOMAIN_MODEL_RULES.md").exists() assert (tmp_path / ".codex" / "ai_rule_developer" / "EXTERNAL_INTEGRATION_RULES.md").exists() assert (tmp_path / ".codex" / "ref_docs").is_dir() @@ -23,7 +24,7 @@ def test_default_generation_success(tmp_path): assert (tmp_path / "docs" / "database" / "schema.md").exists() assert (tmp_path / ".codex" / "codex_start_prompt.txt").exists() assert (tmp_path / "AGENTS.md").exists() - assert len(result.written_files) == 18 + assert len(result.written_files) == 19 start_prompt = (tmp_path / ".codex" / "codex_start_prompt.txt").read_text(encoding="utf-8") assert "지금부터 이 저장소의 작업을 시작한다" in start_prompt @@ -44,7 +45,7 @@ def test_fastapi_profile_generation(tmp_path): CodexBuilder().build(config, target_dir=tmp_path) - service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").read_text(encoding="utf-8") + service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").read_text(encoding="utf-8") architecture_rules = (tmp_path / ".codex" / "ai_rule_developer" / "ARCHITECTURE_RULES.md").read_text(encoding="utf-8") document_rules = (tmp_path / ".codex" / "ai_rule_developer" / "DOCUMENT_RULE.md").read_text(encoding="utf-8") @@ -63,7 +64,7 @@ def test_python_profile_generation(tmp_path): CodexBuilder().build(config, target_dir=tmp_path) architecture_rules = (tmp_path / ".codex" / "ai_rule_developer" / "ARCHITECTURE_RULES.md").read_text(encoding="utf-8") - service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").read_text(encoding="utf-8") + service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").read_text(encoding="utf-8") domain_rules = (tmp_path / ".codex" / "ai_rule_developer" / "DOMAIN_MODEL_RULES.md").read_text(encoding="utf-8") assert "Python" in service_rules @@ -79,7 +80,7 @@ def test_typescript_javascript_profile_generation(tmp_path): CodexBuilder().build(config, target_dir=tmp_path) architecture_rules = (tmp_path / ".codex" / "ai_rule_developer" / "ARCHITECTURE_RULES.md").read_text(encoding="utf-8") - service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").read_text(encoding="utf-8") + service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").read_text(encoding="utf-8") assert "TypeScript/JavaScript" in service_rules assert "Entrypoint -> Application Service -> Domain/Adapter -> External System" in architecture_rules @@ -119,7 +120,7 @@ def test_react_profile_generation(tmp_path): CodexBuilder().build(config, target_dir=tmp_path) architecture_rules = (tmp_path / ".codex" / "ai_rule_developer" / "ARCHITECTURE_RULES.md").read_text(encoding="utf-8") - service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").read_text(encoding="utf-8") + service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").read_text(encoding="utf-8") assert "Page -> Hook/Store -> Service -> API" in architecture_rules assert "컴포넌트 안에서 fetch/axios 호출을 직접 수행하지 않는다" in service_rules diff --git a/tests/test_cli.py b/tests/test_cli.py index 242faa9..66f7d10 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -59,7 +59,7 @@ def test_cli_generates_python_profile(tmp_path): ) assert result == 0 - service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").read_text(encoding="utf-8") + service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").read_text(encoding="utf-8") assert "Python" in service_rules assert "`main()` 진입점" in service_rules @@ -144,7 +144,7 @@ def test_cli_reprompts_invalid_stack_value(tmp_path, monkeypatch): result = main(["--target-dir", str(tmp_path), "--name", "prompt-api"]) assert result == 0 - service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "SERVICE_LAYER_RULES.md").read_text(encoding="utf-8") + service_rules = (tmp_path / ".codex" / "ai_rule_developer" / "GLOBAL_RULES.md").read_text(encoding="utf-8") assert "Next.js" in service_rules diff --git a/tests/test_rule_documents.py b/tests/test_rule_documents.py new file mode 100644 index 0000000..9db4bfa --- /dev/null +++ b/tests/test_rule_documents.py @@ -0,0 +1,78 @@ +"""Tests for rule document composition: no duplication, no missing documents.""" + +from __future__ import annotations + +from collections import Counter +from pathlib import Path + +from codex_builder.models import ProjectConfig +from codex_builder.template_renderer import TemplateRenderer + +RULE_DIR = ".codex/ai_rule_developer" + + +def _render(**kwargs) -> dict[Path, str]: + config = ProjectConfig( + project_name="rule-doc-sample", + stack=kwargs.pop("stack", ("fastapi", "react")), + database="mysql", + auth_enabled=True, + external_api_enabled=True, + docs_level="strict", + **kwargs, + ) + return TemplateRenderer().render(config) + + +def test_test_rules_document_is_generated() -> None: + """`TEST_RULES.md` used to be rendered but never written to disk.""" + + for language in ("ko", "en"): + rendered = _render(language=language) + content = rendered[Path(RULE_DIR) / "TEST_RULES.md"] + + assert "FastAPI" in content + assert "React" in content + assert "mysql" in content + + +def test_framework_rules_are_not_repeated_across_rule_documents() -> None: + """Framework rules belong in GLOBAL_RULES.md only. + + They used to be inlined verbatim into CODE_STYLE_RULES.md and + SERVICE_LAYER_RULES.md as well, which spent context budget without adding + information. + """ + + rendered = _render() + rule_docs = {path: text for path, text in rendered.items() if RULE_DIR in str(path)} + + counts: Counter[str] = Counter() + for text in rule_docs.values(): + for line in text.splitlines(): + stripped = line.strip().lstrip("- ").strip() + # The cross-reference line is meant to appear in several documents. + if len(stripped) > 40 and "GLOBAL_RULES.md" not in stripped: + counts[stripped] += 1 + + repeated = sorted(line for line, count in counts.items() if count > 1) + assert repeated == [], f"규칙 문서 간 중복된 문장: {repeated}" + + +def test_deduplicated_documents_still_point_at_framework_rules() -> None: + rendered = _render() + + for name in ("CODE_STYLE_RULES.md", "SERVICE_LAYER_RULES.md"): + content = rendered[Path(RULE_DIR) / name] + assert "GLOBAL_RULES.md" in content, name + + +def test_every_rendered_document_is_reachable_from_agents_md() -> None: + """A generated rule document nobody is told to read is dead weight.""" + + rendered = _render() + agents_md = rendered[Path("AGENTS.md")] + + for path in rendered: + if RULE_DIR in str(path): + assert path.name in agents_md, f"{path.name}가 AGENTS.md 라우팅 표에 없습니다"