From 4d3b9117b6750672865c1825936c9a310d365b8a Mon Sep 17 00:00:00 2001 From: intelligent04 Date: Tue, 4 Aug 2026 13:33:21 +0000 Subject: [PATCH 1/2] =?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 라우팅 표에 없습니다" From 2c381dad79bebdf4e0d7a2f7144bd3788212d7aa Mon Sep 17 00:00:00 2001 From: intelligent04 Date: Tue, 4 Aug 2026 13:35:04 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EA=B2=80=EC=A6=9D=20=EC=A0=88?= =?UTF-8?q?=EC=B0=A8=EC=99=80=20=EC=95=88=EC=A0=84=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=EC=9D=84=20GLOBAL=5FRULES.md=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 생성된 규칙에 '변경이 실제로 동작하는지 어떻게 확인하는가'가 없었다. AGENTS.md 권장 항목 1순위가 build/test/lint 명령인데 생성물에는 그 지시가 없었다. ## 검증 - 완료 보고 전에 저장소에 정의된 build, test, lint를 실행하게 한다. - 이 도구는 대상 저장소를 스캔하지 않으므로 구체적인 명령을 적지 않는다. 대신 manifest, Makefile, task runner 설정, 기여 문서, CI 워크플로에서 확인하게 하고 정의되지 않은 명령을 지어내는 것을 금지한다. - 실행 불가한 명령을 통과로 가정하지 않게 하고, 테스트를 삭제/skip/단정 약화로 통과시키는 것을 금지한다. ## 안전 - secret, 토큰, 자격 증명, 개인정보를 코드/로그/응답/문서/커밋 메시지에 남기지 않는다. - .env, 자격 증명 파일, CI secret 설정 접근 금지. 새 키가 필요하면 이름만 알린다. - 요청에 없는 파괴적 작업 금지(대량 삭제, git history 재작성, force push, branch 삭제, 데이터 삭제 migration). 불가피하면 실행 전 확인. - lock 파일은 의존성 작업일 때만 수정. 기존에는 secret 관련 문구가 --auth yes일 때만 생성됐다. secret 유출은 인증 기능 유무와 무관하므로 항상 포함되도록 했다. ref_docs 우선순위는 의도된 설계라는 답변을 받아 이 PR에서 건드리지 않았다. tests/test_verification_and_safety.py 7건 추가. 전체 50건 통과. 규칙 문서 크기: fastapi 21,772B -> 23,314B --- CHANGELOG.md | 7 +++ README.md | 2 + codex_builder/template_renderer.py | 63 +++++++++++++++++++ tests/snapshots/default_ko.txt | 18 ++++++ tests/snapshots/fastapi_full_en.txt | 18 ++++++ tests/snapshots/fastapi_react_full_ko.txt | 18 ++++++ tests/snapshots/nextjs_light_ko.txt | 18 ++++++ tests/snapshots/python_standard_en.txt | 18 ++++++ tests/test_verification_and_safety.py | 76 +++++++++++++++++++++++ 9 files changed, 238 insertions(+) create mode 100644 tests/test_verification_and_safety.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 902fb11..4d7023b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Added +- `GLOBAL_RULES.md`에 `## 검증` 섹션 추가. 저장소에 정의된 build/test/lint를 실행하고, 명령은 + manifest·Makefile·CI 워크플로에서 확인하며, 정의되지 않은 명령을 지어내거나 실행하지 못한 것을 + 통과로 가정하지 않도록 지시합니다. 테스트를 삭제·skip·단정 약화로 통과시키는 것을 금지합니다. +- `GLOBAL_RULES.md`에 `## 안전` 섹션 추가. secret·자격 증명 취급, `.env` 및 CI secret 설정 접근 + 금지, 요청에 없는 파괴적 작업(대량 삭제, git history 재작성, force push, 데이터 삭제 migration) + 금지, lock 파일 수정 범위를 규정합니다. 기존에는 secret 관련 문구가 `--auth yes`일 때만 + 생성됐지만 이제 항상 포함됩니다. - `TEST_RULES.md` 생성. 렌더러와 프로필별 테스트 규칙 데이터는 이미 있었지만 출력 목록에 연결되어 있지 않아 실제로는 생성되지 않았습니다. AGENTS.md 라우팅 표에도 추가했습니다. - 루트 `AGENTS.md` 진입점 생성. 코딩 에이전트가 자동으로 읽는 상위 지시문 경로를 확보하기 위한 diff --git a/README.md b/README.md index 5f8c45a..6244b26 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ docs/ 기존 `AGENTS.md`가 있으면 덮어쓰지 않으므로 직접 수정한 내용은 재생성 후에도 유지됩니다. 생성을 원하지 않으면 `--no-agents-md`를 사용하세요. +`GLOBAL_RULES.md`에는 변경을 완료로 보고하기 전 실행할 검증 절차와, secret 취급·파괴적 작업 금지를 다루는 안전 규칙이 포함됩니다. 이 도구는 대상 저장소를 스캔하지 않으므로 구체적인 명령을 적어 넣는 대신 저장소에서 확인하도록 지시하고 없는 명령을 지어내지 못하게 합니다. + 프레임워크별 규칙은 `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 1789967..2e1402d 100644 --- a/codex_builder/template_renderer.py +++ b/codex_builder/template_renderer.py @@ -109,6 +109,8 @@ def _render_global_rules(self, config: ProjectConfig, profiles: tuple[FrameworkP "- Do not add core code without documenting the intent when the behavior is non-obvious.", ] ) + lines.extend(self._verification_rules(config)) + lines.extend(self._safety_rules(config)) return "\n".join(lines) lines = [ @@ -143,8 +145,69 @@ def _render_global_rules(self, config: ProjectConfig, profiles: tuple[FrameworkP "- 동작이 비명시적인 핵심 코드에 의도 설명 없이 코드를 추가하지 않는다.", ] ) + lines.extend(self._verification_rules(config)) + lines.extend(self._safety_rules(config)) return "\n".join(lines) + def _verification_rules(self, config: ProjectConfig) -> list[str]: + """Tell the agent how to prove a change works. + + This generator does not scan the target repository, so it must not + invent commands. Instead it tells the agent where to discover them and + forbids guessing. + """ + + if config.language == "en": + return [ + "", + "## Verification", + "- Run this repository's build, test, and lint commands before calling a change finished.", + "- Discover those commands from the repository itself: package manifests, `Makefile`, task runner", + " configuration, contributor docs, or the CI workflow. Do not invent a command that is not defined.", + "- If a command cannot be run in the current environment, say so explicitly instead of assuming it passed.", + "- If a test fails, report the cause. Never delete a test, skip it, or weaken an assertion to get a green run.", + "- When behavior changes, update or add tests in the same change.", + ] + return [ + "", + "## 검증", + "- 작업을 완료로 보고하기 전에 이 저장소에 정의된 build, test, lint 명령을 실행한다.", + "- 명령은 저장소에서 확인한다. package manifest, `Makefile`, task runner 설정, 기여 문서, CI 워크플로를", + " 확인하고, 정의되어 있지 않은 명령을 임의로 만들어내지 않는다.", + "- 현재 환경에서 실행할 수 없는 명령이 있으면 통과했다고 가정하지 말고 실행하지 못했다고 명시한다.", + "- 테스트가 실패하면 원인을 보고한다. 테스트를 삭제하거나 skip 처리하거나 단정을 약화시켜 통과시키지 않는다.", + "- 동작이 바뀌면 같은 변경에서 테스트를 추가하거나 수정한다.", + ] + + def _safety_rules(self, config: ProjectConfig) -> list[str]: + """Guardrails for actions that are hard or impossible to undo.""" + + if config.language == "en": + return [ + "", + "## Safety", + "- Never write secrets, tokens, credentials, or personal data into code, logs, responses, docs, or commit", + " messages. Use obvious placeholders in examples.", + "- Do not read or edit `.env` files, credential files, or CI secret configuration. If a new key is needed,", + " report the key name and let a human set the value.", + "- Do not perform destructive actions the request did not ask for: bulk file or directory deletion,", + " rewriting git history, force pushing, deleting branches, or data-destroying migrations.", + "- If an irreversible step is genuinely required, describe it and ask before running it.", + "- Change dependency lock files only when the task is about dependencies.", + ] + return [ + "", + "## 안전", + "- secret, 토큰, 자격 증명, 개인정보를 코드, 로그, 응답, 문서, 커밋 메시지에 남기지 않는다.", + " 예시가 필요하면 실제 값 대신 누가 봐도 placeholder임이 분명한 값을 사용한다.", + "- `.env`, 자격 증명 파일, CI secret 설정은 읽거나 수정하지 않는다. 새 키가 필요하면 키 이름만 알리고", + " 값 설정은 사람에게 맡긴다.", + "- 요청에 없는 파괴적 작업을 수행하지 않는다. 파일이나 디렉토리 일괄 삭제, git history 재작성,", + " force push, branch 삭제, 데이터가 사라지는 migration이 여기에 해당한다.", + "- 되돌릴 수 없는 작업이 정말 필요하면 내용을 설명하고 실행 전에 확인을 받는다.", + "- 의존성 lock 파일은 의존성 변경이 작업 목적일 때만 수정한다.", + ] + def _conditional_global_rules(self, config: ProjectConfig) -> list[str]: if config.language == "en": lines: list[str] = [] diff --git a/tests/snapshots/default_ko.txt b/tests/snapshots/default_ko.txt index a35648e..74dcd71 100644 --- a/tests/snapshots/default_ko.txt +++ b/tests/snapshots/default_ko.txt @@ -313,6 +313,24 @@ docs/database - 외부 연동은 확인된 계약을 기준으로 구현하고 실패 동작을 명시한다. - 동작이 비명시적인 핵심 코드에 의도 설명 없이 코드를 추가하지 않는다. +## 검증 +- 작업을 완료로 보고하기 전에 이 저장소에 정의된 build, test, lint 명령을 실행한다. +- 명령은 저장소에서 확인한다. package manifest, `Makefile`, task runner 설정, 기여 문서, CI 워크플로를 + 확인하고, 정의되어 있지 않은 명령을 임의로 만들어내지 않는다. +- 현재 환경에서 실행할 수 없는 명령이 있으면 통과했다고 가정하지 말고 실행하지 못했다고 명시한다. +- 테스트가 실패하면 원인을 보고한다. 테스트를 삭제하거나 skip 처리하거나 단정을 약화시켜 통과시키지 않는다. +- 동작이 바뀌면 같은 변경에서 테스트를 추가하거나 수정한다. + +## 안전 +- secret, 토큰, 자격 증명, 개인정보를 코드, 로그, 응답, 문서, 커밋 메시지에 남기지 않는다. + 예시가 필요하면 실제 값 대신 누가 봐도 placeholder임이 분명한 값을 사용한다. +- `.env`, 자격 증명 파일, CI secret 설정은 읽거나 수정하지 않는다. 새 키가 필요하면 키 이름만 알리고 + 값 설정은 사람에게 맡긴다. +- 요청에 없는 파괴적 작업을 수행하지 않는다. 파일이나 디렉토리 일괄 삭제, git history 재작성, + force push, branch 삭제, 데이터가 사라지는 migration이 여기에 해당한다. +- 되돌릴 수 없는 작업이 정말 필요하면 내용을 설명하고 실행 전에 확인을 받는다. +- 의존성 lock 파일은 의존성 변경이 작업 목적일 때만 수정한다. + ======================================================================== # file: .codex/ai_rule_developer/SERVICE_LAYER_RULES.md ======================================================================== diff --git a/tests/snapshots/fastapi_full_en.txt b/tests/snapshots/fastapi_full_en.txt index ff19001..8e81c07 100644 --- a/tests/snapshots/fastapi_full_en.txt +++ b/tests/snapshots/fastapi_full_en.txt @@ -351,6 +351,24 @@ Before changing files, read the `.codex` documents and treat them as repository- - Implement external integrations against their confirmed contracts and keep failure behavior explicit. - Do not add core code without documenting the intent when the behavior is non-obvious. +## Verification +- Run this repository's build, test, and lint commands before calling a change finished. +- Discover those commands from the repository itself: package manifests, `Makefile`, task runner + configuration, contributor docs, or the CI workflow. Do not invent a command that is not defined. +- If a command cannot be run in the current environment, say so explicitly instead of assuming it passed. +- If a test fails, report the cause. Never delete a test, skip it, or weaken an assertion to get a green run. +- When behavior changes, update or add tests in the same change. + +## Safety +- Never write secrets, tokens, credentials, or personal data into code, logs, responses, docs, or commit + messages. Use obvious placeholders in examples. +- Do not read or edit `.env` files, credential files, or CI secret configuration. If a new key is needed, + report the key name and let a human set the value. +- Do not perform destructive actions the request did not ask for: bulk file or directory deletion, + rewriting git history, force pushing, deleting branches, or data-destroying migrations. +- If an irreversible step is genuinely required, describe it and ask before running it. +- Change dependency lock files only when the task is about dependencies. + ======================================================================== # file: .codex/ai_rule_developer/SERVICE_LAYER_RULES.md ======================================================================== diff --git a/tests/snapshots/fastapi_react_full_ko.txt b/tests/snapshots/fastapi_react_full_ko.txt index 31e8bff..616cf70 100644 --- a/tests/snapshots/fastapi_react_full_ko.txt +++ b/tests/snapshots/fastapi_react_full_ko.txt @@ -386,6 +386,24 @@ docs/database - 외부 연동은 확인된 계약을 기준으로 구현하고 실패 동작을 명시한다. - 동작이 비명시적인 핵심 코드에 의도 설명 없이 코드를 추가하지 않는다. +## 검증 +- 작업을 완료로 보고하기 전에 이 저장소에 정의된 build, test, lint 명령을 실행한다. +- 명령은 저장소에서 확인한다. package manifest, `Makefile`, task runner 설정, 기여 문서, CI 워크플로를 + 확인하고, 정의되어 있지 않은 명령을 임의로 만들어내지 않는다. +- 현재 환경에서 실행할 수 없는 명령이 있으면 통과했다고 가정하지 말고 실행하지 못했다고 명시한다. +- 테스트가 실패하면 원인을 보고한다. 테스트를 삭제하거나 skip 처리하거나 단정을 약화시켜 통과시키지 않는다. +- 동작이 바뀌면 같은 변경에서 테스트를 추가하거나 수정한다. + +## 안전 +- secret, 토큰, 자격 증명, 개인정보를 코드, 로그, 응답, 문서, 커밋 메시지에 남기지 않는다. + 예시가 필요하면 실제 값 대신 누가 봐도 placeholder임이 분명한 값을 사용한다. +- `.env`, 자격 증명 파일, CI secret 설정은 읽거나 수정하지 않는다. 새 키가 필요하면 키 이름만 알리고 + 값 설정은 사람에게 맡긴다. +- 요청에 없는 파괴적 작업을 수행하지 않는다. 파일이나 디렉토리 일괄 삭제, git history 재작성, + force push, branch 삭제, 데이터가 사라지는 migration이 여기에 해당한다. +- 되돌릴 수 없는 작업이 정말 필요하면 내용을 설명하고 실행 전에 확인을 받는다. +- 의존성 lock 파일은 의존성 변경이 작업 목적일 때만 수정한다. + ======================================================================== # file: .codex/ai_rule_developer/SERVICE_LAYER_RULES.md ======================================================================== diff --git a/tests/snapshots/nextjs_light_ko.txt b/tests/snapshots/nextjs_light_ko.txt index c8da362..abceb02 100644 --- a/tests/snapshots/nextjs_light_ko.txt +++ b/tests/snapshots/nextjs_light_ko.txt @@ -310,6 +310,24 @@ docs/database - 외부 연동은 확인된 계약을 기준으로 구현하고 실패 동작을 명시한다. - 동작이 비명시적인 핵심 코드에 의도 설명 없이 코드를 추가하지 않는다. +## 검증 +- 작업을 완료로 보고하기 전에 이 저장소에 정의된 build, test, lint 명령을 실행한다. +- 명령은 저장소에서 확인한다. package manifest, `Makefile`, task runner 설정, 기여 문서, CI 워크플로를 + 확인하고, 정의되어 있지 않은 명령을 임의로 만들어내지 않는다. +- 현재 환경에서 실행할 수 없는 명령이 있으면 통과했다고 가정하지 말고 실행하지 못했다고 명시한다. +- 테스트가 실패하면 원인을 보고한다. 테스트를 삭제하거나 skip 처리하거나 단정을 약화시켜 통과시키지 않는다. +- 동작이 바뀌면 같은 변경에서 테스트를 추가하거나 수정한다. + +## 안전 +- secret, 토큰, 자격 증명, 개인정보를 코드, 로그, 응답, 문서, 커밋 메시지에 남기지 않는다. + 예시가 필요하면 실제 값 대신 누가 봐도 placeholder임이 분명한 값을 사용한다. +- `.env`, 자격 증명 파일, CI secret 설정은 읽거나 수정하지 않는다. 새 키가 필요하면 키 이름만 알리고 + 값 설정은 사람에게 맡긴다. +- 요청에 없는 파괴적 작업을 수행하지 않는다. 파일이나 디렉토리 일괄 삭제, git history 재작성, + force push, branch 삭제, 데이터가 사라지는 migration이 여기에 해당한다. +- 되돌릴 수 없는 작업이 정말 필요하면 내용을 설명하고 실행 전에 확인을 받는다. +- 의존성 lock 파일은 의존성 변경이 작업 목적일 때만 수정한다. + ======================================================================== # file: .codex/ai_rule_developer/SERVICE_LAYER_RULES.md ======================================================================== diff --git a/tests/snapshots/python_standard_en.txt b/tests/snapshots/python_standard_en.txt index 15067db..1e1c6fe 100644 --- a/tests/snapshots/python_standard_en.txt +++ b/tests/snapshots/python_standard_en.txt @@ -312,6 +312,24 @@ Before changing files, read the `.codex` documents and treat them as repository- - Implement external integrations against their confirmed contracts and keep failure behavior explicit. - Do not add core code without documenting the intent when the behavior is non-obvious. +## Verification +- Run this repository's build, test, and lint commands before calling a change finished. +- Discover those commands from the repository itself: package manifests, `Makefile`, task runner + configuration, contributor docs, or the CI workflow. Do not invent a command that is not defined. +- If a command cannot be run in the current environment, say so explicitly instead of assuming it passed. +- If a test fails, report the cause. Never delete a test, skip it, or weaken an assertion to get a green run. +- When behavior changes, update or add tests in the same change. + +## Safety +- Never write secrets, tokens, credentials, or personal data into code, logs, responses, docs, or commit + messages. Use obvious placeholders in examples. +- Do not read or edit `.env` files, credential files, or CI secret configuration. If a new key is needed, + report the key name and let a human set the value. +- Do not perform destructive actions the request did not ask for: bulk file or directory deletion, + rewriting git history, force pushing, deleting branches, or data-destroying migrations. +- If an irreversible step is genuinely required, describe it and ask before running it. +- Change dependency lock files only when the task is about dependencies. + ======================================================================== # file: .codex/ai_rule_developer/SERVICE_LAYER_RULES.md ======================================================================== diff --git a/tests/test_verification_and_safety.py b/tests/test_verification_and_safety.py new file mode 100644 index 0000000..d1e7062 --- /dev/null +++ b/tests/test_verification_and_safety.py @@ -0,0 +1,76 @@ +"""Tests for the verification and safety sections of GLOBAL_RULES.md.""" + +from __future__ import annotations + +from pathlib import Path + +from codex_builder.models import ProjectConfig +from codex_builder.template_renderer import TemplateRenderer + +GLOBAL_RULES = Path(".codex/ai_rule_developer/GLOBAL_RULES.md") + + +def _global_rules(language: str = "ko", **kwargs) -> str: + config = ProjectConfig(project_name="safety-sample", language=language, **kwargs) + return TemplateRenderer().render(config)[GLOBAL_RULES] + + +def test_verification_section_exists_in_both_languages() -> None: + assert "## 검증" in _global_rules("ko") + assert "## Verification" in _global_rules("en") + + +def test_agent_is_told_to_run_repository_commands() -> None: + content = _global_rules("ko") + + assert "build, test, lint" in content + assert "CI 워크플로" in content + + +def test_agent_must_not_invent_commands_or_assume_success() -> None: + """The generator never scans the target repo, so guessing must be forbidden.""" + + ko = _global_rules("ko") + en = _global_rules("en") + + assert "임의로 만들어내지 않는다" in ko + assert "통과했다고 가정하지 말고" in ko + assert "Do not invent a command that is not defined." in en + assert "instead of assuming it passed" in en + + +def test_agent_may_not_weaken_tests_to_get_a_green_run() -> None: + ko = _global_rules("ko") + + assert "단정을 약화시켜 통과시키지 않는다" in ko + assert "skip 처리" in ko + + +def test_safety_section_covers_secrets() -> None: + for language, needle in (("ko", "## 안전"), ("en", "## Safety")): + content = _global_rules(language) + assert needle in content + + ko = _global_rules("ko") + assert "`.env`" in ko + assert "커밋 메시지에 남기지 않는다" in ko + assert "값 설정은 사람에게 맡긴다" in ko + + +def test_safety_section_covers_destructive_actions() -> None: + ko = _global_rules("ko") + + for phrase in ("git history 재작성", "force push", "branch 삭제", "migration"): + assert phrase in ko, phrase + + assert "실행 전에 확인을 받는다" in ko + assert "lock 파일" in ko + + +def test_safety_rules_apply_even_without_auth_or_external_api() -> None: + """Secret handling used to appear only when --auth was enabled.""" + + content = _global_rules("ko", auth_enabled=False, external_api_enabled=False) + + assert "## 안전" in content + assert "secret" in content