modify framework

This commit is contained in:
김경종
2026-06-02 09:51:30 +09:00
parent 88d8613847
commit a292238675
13 changed files with 602 additions and 169 deletions
+16 -38
View File
@@ -1,14 +1,10 @@
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
CHECKS = ("lint", "build", "test")
def _repo_root(cwd: Path) -> Path:
try:
root = subprocess.check_output(
@@ -22,25 +18,11 @@ def _repo_root(cwd: Path) -> Path:
return Path(root)
def _load_scripts(root: Path) -> dict[str, str]:
package_json = root / "package.json"
if not package_json.exists():
return {}
try:
package = json.loads(package_json.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
_deny(f"Invalid package.json: {exc}")
raise SystemExit(0) from exc
scripts = package.get("scripts", {})
if not isinstance(scripts, dict):
return {}
return {str(name): str(command) for name, command in scripts.items()}
def _is_git_commit(command: str) -> bool:
return re.search(r"\bgit(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b", command) is not None
return re.search(
r"^\s*git(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b",
command,
) is not None
def _deny(reason: str) -> None:
@@ -64,26 +46,22 @@ def _tail(text: str, limit: int = 1200) -> str:
return text[-limit:]
def _run_checks(root: Path, scripts: dict[str, str]) -> str | None:
npm = shutil.which("npm") or shutil.which("npm.cmd")
if npm is None:
return "npm was not found, so pre-commit checks could not run."
def _build_pre_commit_commands(root: Path) -> list[list[str]]:
return [
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
[sys.executable, "scripts/validate_workspace.py"],
]
for check in CHECKS:
if check not in scripts:
continue
result = subprocess.run(
[npm, "run", check],
cwd=root,
capture_output=True,
text=True,
)
def _run_checks(root: Path) -> str | None:
for command in _build_pre_commit_commands(root):
result = subprocess.run(command, cwd=root, capture_output=True, text=True)
if result.returncode != 0:
details = _tail(result.stdout + "\n" + result.stderr)
label = " ".join(command)
if details:
return f"npm run {check} failed:\n{details}"
return f"npm run {check} failed with exit code {result.returncode}."
return f"{label} failed:\n{details}"
return f"{label} failed with exit code {result.returncode}."
return None
@@ -100,7 +78,7 @@ def main() -> int:
cwd = Path(payload.get("cwd") or Path.cwd())
root = _repo_root(cwd)
failure = _run_checks(root, _load_scripts(root))
failure = _run_checks(root)
if failure:
_deny(f"PRE-COMMIT CHECKS: {failure}")
+67 -51
View File
@@ -4,19 +4,9 @@ import sys
from pathlib import Path
SOURCE_SUFFIXES = {".ts", ".tsx", ".js", ".jsx"}
TEST_SUFFIXES = ("ts", "tsx", "js", "jsx")
CONFIG_SUFFIXES = {".json", ".css", ".scss", ".md", ".yml", ".yaml"}
NEXT_SPECIAL_FILES = {
"layout.ts",
"layout.tsx",
"page.ts",
"page.tsx",
"loading.tsx",
"error.tsx",
"not-found.tsx",
"globals.css",
}
SOURCE_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
TEST_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
CONFIG_SUFFIXES = {".json", ".md", ".yml", ".yaml", ".txt", ".cmake"}
def _repo_root(cwd: Path) -> Path:
@@ -72,13 +62,66 @@ def _normalize(path_text: str) -> str:
def _is_test_path(path_text: str) -> bool:
normalized = _normalize(path_text)
name = normalized.rsplit("/", 1)[-1]
path = Path(path_text)
return (
"__tests__/" in normalized
"/tests/" in f"/{normalized}"
or "/test/" in f"/{normalized}"
or name.endswith("_test.cpp")
or name.startswith("test_")
or ".test." in name
or ".spec." in name
or "test" in name
or "spec" in name
)
) and path.suffix.lower() in TEST_SUFFIXES
def _token(text: str) -> str:
return "".join(ch for ch in text.lower() if ch.isalnum())
def _module_token(path: Path) -> str:
parts = [part.lower() for part in path.parts]
for marker in ("include", "src"):
if marker not in parts:
continue
idx = parts.index(marker)
if marker == "include" and idx + 2 < len(parts) and parts[idx + 1] == "fesa":
return _token(parts[idx + 2])
if marker == "src" and idx + 1 < len(parts):
return _token(parts[idx + 1])
return ""
def _related_tokens(path: Path) -> set[str]:
tokens = {_token(_base_name(path))}
module = _module_token(path)
if module:
tokens.add(module)
return {token for token in tokens if token}
def _candidate_test_paths(paths: list[str], cwd: Path, root: Path) -> list[Path]:
candidates: list[Path] = []
for path_text in paths:
resolved = _resolve_path(path_text, cwd)
if _is_test_path(str(resolved)):
candidates.append(resolved)
for test_root_name in ("tests", "test"):
test_root = root / test_root_name
if not test_root.is_dir():
continue
for suffix in TEST_SUFFIXES:
candidates.extend(test_root.rglob(f"*{suffix}"))
return candidates
def _has_related_test(path: Path, candidate_tests: list[Path]) -> bool:
tokens = _related_tokens(path)
for test_path in candidate_tests:
test_token = _token(test_path.stem)
if any(token and token in test_token for token in tokens):
return True
return False
def _is_exempt(path_text: str) -> bool:
@@ -86,17 +129,13 @@ def _is_exempt(path_text: str) -> bool:
path = Path(path_text)
name = path.name.lower()
if _is_test_path(path_text):
if name == "cmakelists.txt":
return True
if name in NEXT_SPECIAL_FILES:
if _is_test_path(path_text):
return True
if path.suffix.lower() in CONFIG_SUFFIXES:
return True
if ".env" in name or ".config." in name:
return True
if any(token in name for token in ("tailwind", "postcss", "next.config", "tsconfig")):
return True
if "/types/" in normalized or name in {"types.ts", "types.d.ts"}:
if "/cmake/" in normalized:
return True
return False
@@ -110,38 +149,15 @@ def _resolve_path(path_text: str, cwd: Path) -> Path:
def _base_name(path: Path) -> str:
for suffix in (".tsx", ".ts", ".jsx", ".js"):
if path.name.endswith(suffix):
for suffix in sorted(SOURCE_SUFFIXES, key=len, reverse=True):
if path.name.lower().endswith(suffix):
return path.name[: -len(suffix)]
return path.stem
def _has_existing_test(path: Path, root: Path) -> bool:
directory = path.parent
parent = directory.parent
base = _base_name(path)
for ext in TEST_SUFFIXES:
if (directory / f"{base}.test.{ext}").exists():
return True
if (directory / f"{base}.spec.{ext}").exists():
return True
for ext in TEST_SUFFIXES:
if (parent / "__tests__" / f"{base}.test.{ext}").exists():
return True
if (directory / "__tests__" / f"{base}.test.{ext}").exists():
return True
for ext in TEST_SUFFIXES:
if (root / "src" / "__tests__" / f"{base}.test.{ext}").exists():
return True
return False
def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
missing_tests: list[str] = []
candidate_tests = _candidate_test_paths(paths, cwd, root)
for path_text in paths:
if _is_exempt(path_text):
continue
@@ -149,7 +165,7 @@ def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
path = _resolve_path(path_text, cwd)
if path.suffix.lower() not in SOURCE_SUFFIXES:
continue
if not _has_existing_test(path, root):
if not _has_related_test(path, candidate_tests):
missing_tests.append(_base_name(path))
return missing_tests
+10 -8
View File
@@ -1,19 +1,19 @@
---
name: harness-review
description: Use when reviewing this Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, build readiness, or compliance with AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and Harness acceptance criteria.
description: Use when reviewing this C++/MSVC Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, MSVC build readiness, or compliance with AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and Harness acceptance criteria.
---
# Harness Review
## Overview
Use this skill to review Harness work against the repository's persistent rules, architecture docs, and executable verification requirements. Prioritize bugs, regressions, missing tests, and rule violations.
Use this skill to review Harness work against the repository's persistent rules, architecture docs, C++/MSVC constraints, TDD guard policy, and executable verification requirements. Prioritize bugs, regressions, missing tests, and rule violations.
## Review Process
1. Read `/AGENTS.md`, `/docs/ARCHITECTURE.md`, and `/docs/ADR.md`.
2. Inspect the changed files with `git status --short` and `git diff`.
3. Check architecture, stack choices, tests, critical rules, and build readiness.
3. Check architecture, stack choices, C++ test coverage, critical rules, and MSVC/CMake readiness.
4. Run relevant verification commands when feasible. If a command cannot be run, report that as residual risk.
5. Lead with actionable findings. Keep summaries secondary.
@@ -21,11 +21,12 @@ Use this skill to review Harness work against the repository's persistent rules,
| Item | Question |
| --- | --- |
| Architecture | Does the change follow `docs/ARCHITECTURE.md` directory and module boundaries? |
| Stack | Does the change stay within choices documented in `docs/ADR.md`? |
| Tests | Are new or changed behaviors covered by tests? |
| Architecture | Does the change follow `docs/ARCHITECTURE.md` ownership boundaries? |
| Stack | Does the change stay within C++/MSVC/CMake decisions documented in `docs/ADR.md`? |
| Tests | Are new or changed behaviors covered by Python Harness tests or C++ tests? |
| TDD Guard | Would C++ production edits be blocked without related tests? |
| Critical Rules | Does the change violate any `AGENTS.md` CRITICAL rule? |
| Build | Do relevant build/test/lint commands pass? |
| Build | Do `python -m unittest discover -s scripts -p "test_*.py"` and `python scripts/validate_workspace.py` pass or provide an expected no-CMake message? |
## Output Format
@@ -36,7 +37,8 @@ If there are findings, list them first in severity order with file and line refe
| 아키텍처 준수 | PASS/FAIL | {상세} |
| 기술 스택 준수 | PASS/FAIL | {상세} |
| 테스트 존재 | PASS/FAIL | {상세} |
| TDD Guard | PASS/FAIL | {상세} |
| CRITICAL 규칙 | PASS/FAIL | {상세} |
| 빌드 가능 | PASS/FAIL | {상세} |
| 빌드/검증 가능 | PASS/FAIL | {상세} |
When there are no findings, say that clearly, then mention any commands not run or remaining risk.
+16 -11
View File
@@ -1,13 +1,13 @@
---
name: harness-workflow
description: Use when planning or running this Harness framework: reading AGENTS.md and docs/*.md, discussing implementation scope, creating or updating phases/index.json, phases/{task}/index.json, phases/{task}/stepN.md, or invoking scripts/execute.py for staged Codex execution.
description: Use when planning or running this C++/MSVC Harness framework: reading AGENTS.md and docs/*.md, discussing implementation scope, creating or updating phases/index.json, phases/{task}/index.json, phases/{task}/stepN.md, or invoking scripts/execute.py for staged Codex execution.
---
# Harness Workflow
## Overview
Use this skill to turn a user-approved task into small, self-contained Harness steps that another Codex session can execute reliably. Keep the workflow grounded in repository docs and executable acceptance criteria.
Use this skill to turn a user-approved task into small, self-contained Harness steps that another Codex session can execute reliably. Keep every step grounded in repository docs, C++/MSVC constraints, TDD, and executable acceptance criteria.
## Workflow
@@ -23,10 +23,10 @@ Use this skill to turn a user-approved task into small, self-contained Harness s
- Make every step self-contained. Do not rely on prior conversation; include all required context and file paths.
- Force context gathering. Each step must tell Codex which docs and previous outputs to read before editing.
- Specify interfaces and signatures, not full implementations, unless exact code is required for a constraint.
- Put core invariants directly in the step: idempotency, security, data integrity, API contracts, or other non-negotiables.
- Use executable acceptance criteria such as `npm run build && npm test`, not abstract statements.
- Write cautions concretely: "Do not do X. Reason: Y."
- Name steps with kebab-case slugs such as `project-setup`, `api-layer`, or `auth-flow`.
- Put core invariants directly in the step: idempotency, numerical conventions, data integrity, API contracts, or other non-negotiables.
- Use executable acceptance criteria such as `python scripts/validate_workspace.py`, not abstract statements.
- For C++ behavior changes, require tests first and name the expected test file or test executable.
- Name steps with kebab-case slugs such as `project-setup`, `core-types`, or `solver-validation`.
## Phase Files
@@ -47,12 +47,12 @@ Create `phases/{task-name}/index.json`:
```json
{
"project": "<project-name>",
"project": "FESA Harness",
"phase": "<task-name>",
"steps": [
{ "step": 0, "name": "project-setup", "status": "pending" },
{ "step": 1, "name": "core-types", "status": "pending" },
{ "step": 2, "name": "api-layer", "status": "pending" }
{ "step": 2, "name": "validation-path", "status": "pending" }
]
}
```
@@ -85,11 +85,15 @@ Rules:
{Concrete instructions with file paths, interfaces, signatures, and rules.}
## Tests To Write First
- {Exact C++ or Python test file and behavior to add before implementation.}
## Acceptance Criteria
```bash
npm run build
npm test
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
```
## 검증 절차
@@ -99,6 +103,7 @@ npm test
- ARCHITECTURE.md 디렉토리 구조를 따르는가?
- ADR 기술 스택을 벗어나지 않았는가?
- AGENTS.md CRITICAL 규칙을 위반하지 않았는가?
- C++ 변경에는 관련 테스트가 존재하는가?
3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다:
- 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"`
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
@@ -106,7 +111,7 @@ npm test
## 금지사항
- {Do not do X. Reason: Y.}
- JavaScript/TypeScript/npm fallback을 추가하지 마라. Reason: 이 Harness는 C++/MSVC 전용이다.
- 기존 테스트를 깨뜨리지 마라.
```
+18 -4
View File
@@ -1,8 +1,22 @@
node_modules/
.next/
.vs/
build/
out/
next-env.d.ts
tsconfig.tsbuildinfo
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
CTestTestfile.cmake
Testing/
*.vcxproj.user
*.obj
*.pdb
*.ilk
*.exe
*.dll
*.lib
*.exp
*.log
__pycache__/
*.pyc
# phase execution outputs
phases/**/phase*-output.json
+36 -13
View File
@@ -1,21 +1,44 @@
# 프로젝트: {프로젝트명}
# Project: FESA Harness
## 기술 스택
- {프레임워크 (예: Next.js 15)}
- {언어 (예: TypeScript strict mode)}
- {스타일링 (예: Tailwind CSS)}
- C++17 이상
- MSVC on Windows
- CMake + CTest
- Harness scripts in Python 3
## 아키텍처 규칙
- CRITICAL: {절대 지켜야 할 규칙 1 (예: 모든 API 로직은 app/api/ 라우트 핸들러에서만 처리)}
- CRITICAL: {절대 지켜야 할 규칙 2 (예: 클라이언트 컴포넌트에서 직접 외부 API를 호출하지 말 것)}
- {일반 규칙 (예: 컴포넌트는 components/ 폴더에, 타입은 types/ 폴더에 분리)}
- CRITICAL: 기본 검증 경로는 `python scripts/validate_workspace.py`이다.
- CRITICAL: C++ 빌드는 CMake/MSVC/x64/Debug 기준으로 검증한다.
- CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다.
- CRITICAL: Abaqus reference artifact나 solver 코드 복원은 명시적으로 요청된 phase에서만 수행한다.
- Harness runner는 `scripts/execute.py`에 둔다.
- Codex hook 정책은 `.codex/hooks/`에 둔다.
- Harness planning/review instructions are stored in `.codex/skills/`.
- Generated phase execution outputs remain ignored under `phases/**/step*-output.json`.
## 개발 프로세스
- CRITICAL: 새 기능 구현 시 반드시 테스트를 먼저 작성하고, 테스트가 통과하는 구현을 작성할 것 (TDD)
- 커밋 메시지는 conventional commits 형식을 따를 것 (feat:, fix:, docs:, refactor:)
- TDD를 기본으로 한다. C++ production file을 바꿀 때는 관련 C++ test file이 있어야 한다.
- 커밋 전 hook은 Harness Python self-test와 workspace validation을 실행해야 한다.
- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`.
- 계획이 필요한 장기 작업은 Harness phase로 나누고, 각 step은 독립 실행 가능해야 한다.
## 명령어
npm run dev # 개발 서버
npm run build # 프로덕션 빌드
npm run lint # ESLint
npm run test # 테스트
```bash
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
python scripts/execute.py <phase-dir>
python scripts/execute.py <phase-dir> --push
```
## MSVC 검증 기본값
- Generator: `Visual Studio 17 2022`
- Platform: `x64`
- Config: `Debug`
- Build directory: `build/msvc-debug`
Override variables:
- `HARNESS_VALIDATION_COMMANDS`
- `HARNESS_CMAKE_GENERATOR`
- `HARNESS_CMAKE_PLATFORM`
- `HARNESS_CMAKE_CONFIG`
- `HARNESS_BUILD_DIR`
+26 -13
View File
@@ -1,21 +1,34 @@
# Architecture Decision Records
## 철학
{프로젝트의 핵심 가치관 (예: MVP 속도 최우선. 외부 의존성 최소화. 작동하는 최소 구현을 선택.)}
Harness는 현재 프로젝트의 실제 기술 스택을 반영해야 한다. C++/MSVC 프로젝트에서 npm, Next.js, TypeScript test naming을 기본값으로 두면 agent prompt와 hook policy가 잘못된 구현을 유도한다.
---
### ADR-001: {결정 사항 (예: Next.js App Router 선택)}
**결정**: {뭘 선택했는지}
**이유**: {왜 선택했는지}
**트레이드오프**: {뭘 포기했는지}
### ADR-001: C++ 전용 Harness
**결정**: Harness scaffold는 C++/MSVC 전용으로 운영한다. JavaScript/TypeScript fallback은 유지하지 않는다.
### ADR-002: {결정 사항}
**결정**: {뭘 선택했는지}
**이유**: {왜 선택했는지}
**트레이드오프**: {뭘 포기했는지}
**이유**: FESA 개발 환경은 MSVC 기반 C++이다. 언어별 fallback을 남기면 validation, TDD guard, acceptance criteria가 흐려진다.
### ADR-003: {결정 사항}
**결정**: {뭘 선택했는지}
**이유**: {왜 선택했는지}
**트레이드오프**: {뭘 포기했는지}
**트레이드오프**: 같은 Harness scaffold를 JS/TS 프로젝트에 재사용할 수 없다. 필요하면 별도 template이나 language registry를 새 ADR로 설계한다.
### ADR-002: CMake/MSVC/x64/Debug 기본 검증
**결정**: 기본 workspace validation은 CMake, Visual Studio 17 2022 generator, x64 platform, Debug config, CTest로 수행한다.
**이유**: MSVC 환경에서 CMake/CTest는 source tree가 복원되거나 새 C++ project가 추가될 때 가장 일관된 build/test entry point다.
**트레이드오프**: Visual Studio solution-only project는 기본 지원하지 않는다. 명시적으로 필요하면 `HARNESS_VALIDATION_COMMANDS`로 override한다.
### ADR-003: 엄격한 C++ TDD Guard
**결정**: C++ production file 변경은 관련 C++ test file이 없으면 차단한다.
**이유**: Harness의 핵심 목적은 agent가 검증 없는 C++ 변경을 만들지 않도록 하는 것이다. Header 중심 C++ 구조에서도 module 또는 basename 기반 테스트 존재를 확인한다.
**트레이드오프**: 초기 scaffolding 작업에서 guard가 엄격하게 느껴질 수 있다. 문서, CMake 설정, Harness metadata는 guard 대상에서 제외한다.
### ADR-004: Harness 자체 테스트 우선
**결정**: commit hook은 먼저 Python Harness self-test를 실행한 뒤 workspace validation을 실행한다.
**이유**: 현재 저장소에는 C++ source tree가 없을 수 있다. Harness가 스스로 검증 가능해야 이후 phase generation과 source restoration을 안전하게 진행할 수 있다.
**트레이드오프**: commit 시간이 조금 늘어난다. 대신 hook/validation regressions를 빠르게 잡는다.
+51 -17
View File
@@ -1,24 +1,58 @@
# 아키텍처
## 디렉토리 구조
```
src/
├── app/ # 페이지 + API 라우트
├── components/ # UI 컴포넌트
├── types/ # TypeScript 타입 정의
├── lib/ # 유틸리티 + 헬퍼
└── services/ # 외부 API 래퍼
```
## 목표
이 저장소의 현재 책임은 C++/MSVC 프로젝트를 위한 Codex Harness scaffold를 제공하는 것이다. Harness는 phase execution, edit guard, commit validation, workspace validation을 분리해서 관리한다.
## 패턴
{사용하는 디자인 패턴 (예: Server Components 기본, 인터랙션이 필요한 곳만 Client Component)}
## 디렉토리 구조
```text
.codex/
├── hooks/ # Codex hook scripts
└── skills/ # Harness planning/review instructions
docs/ # Project and Harness guidance
scripts/
├── execute.py # Phase step executor
├── validate_workspace.py
└── test_*.py # Harness self-tests
phases/ # Optional generated phase plans
```
## 데이터 흐름
```
{데이터가 어떻게 흐르는지 (예:
사용자 입력 → Client Component → API Route → 외부 API → 응답 → UI 업데이트
)}
```text
User-approved task
-> Harness phase files under phases/
-> scripts/execute.py injects AGENTS.md and docs/*.md
-> Codex executes one step at a time
-> step updates phases/{phase}/index.json
-> validation runs through scripts/validate_workspace.py
```
## 상태 관리
{상태 관리 방식 (예: 서버 상태는 Server Components, 클라이언트 상태는 useState/useReducer)}
## Hook 흐름
```text
apply_patch/Edit/Write
-> .codex/hooks/tdd-guard.py
-> C++ production changes require related tests
git commit command
-> .codex/hooks/pre_commit_checks.py
-> Python Harness self-tests
-> scripts/validate_workspace.py
```
## Validation 흐름
```text
HARNESS_VALIDATION_COMMANDS set
-> run exact commands
CMakePresets.json has msvc-debug configure preset
-> cmake --preset msvc-debug
-> cmake --build preset binary dir --config Debug
-> ctest --test-dir preset binary dir -C Debug
CMakeLists.txt exists
-> cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
-> cmake --build build/msvc-debug --config Debug
-> ctest --test-dir build/msvc-debug --output-on-failure -C Debug
No CMake project
-> print guidance and exit successfully
```
+15 -14
View File
@@ -1,21 +1,22 @@
# PRD: {프로젝트명}
# PRD: C++/MSVC Harness
## 목표
{이 프로젝트가 해결하려는 문제를 한 줄로 요약}
Codex Harness가 C++/MSVC 프로젝트에서 phase planning, TDD guard, commit validation, workspace validation을 일관되게 수행하게 한다.
## 사용자
{누가 이 제품을 쓰는지}
- Windows/MSVC 기반 C++ 개발자
- Harness phase를 작성하고 실행하는 Codex agent
- Harness 결과를 검토하는 reviewer
## 핵심 기능
1. {기능 1}
2. {기능 2}
3. {기능 3}
1. CMake/MSVC/x64/Debug 기반 workspace validation
2. C++ source/header 변경에 대한 엄격한 TDD guard
3. npm 없이 Python self-test와 CMake/CTest 검증을 수행하는 pre-commit hook
4. C++ 프로젝트에 맞는 Harness workflow/review prompt
5. CMake project가 아직 없어도 Harness 자체 테스트가 가능한 no-op validation path
## MVP 제외 사항
- {안 만들 것 1}
- {안 만들 것 2}
- {안 만들 것 3}
## 디자인
- {디자인 방향 (예: 다크모드 고정, 미니멀)}
- {색상 (예: 무채색 + 포인트 1가지)}
## 제외 사항
- 이전 FESA solver source tree 복원
- JavaScript/TypeScript fallback 유지
- Abaqus reference artifact 생성 또는 solver reference 비교 구현
- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow
+41
View File
@@ -0,0 +1,41 @@
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
def load_pre_commit_checks():
module_path = Path(__file__).resolve().parent.parent / ".codex" / "hooks" / "pre_commit_checks.py"
spec = importlib.util.spec_from_file_location("pre_commit_checks", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class PreCommitChecksTests(unittest.TestCase):
def test_git_commit_runs_python_self_tests_and_workspace_validation(self):
pre_commit_checks = load_pre_commit_checks()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
commands = pre_commit_checks._build_pre_commit_commands(root)
self.assertEqual(
commands,
[
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
[sys.executable, "scripts/validate_workspace.py"],
],
)
self.assertFalse(any("npm" in part.lower() for command in commands for part in command))
def test_only_git_commit_commands_trigger_checks(self):
pre_commit_checks = load_pre_commit_checks()
self.assertTrue(pre_commit_checks._is_git_commit('git commit -m "change"'))
self.assertTrue(pre_commit_checks._is_git_commit('git -c core.editor=true commit -m "change"'))
self.assertFalse(pre_commit_checks._is_git_commit("git status --short"))
self.assertFalse(pre_commit_checks._is_git_commit("echo git commit"))
if __name__ == "__main__":
unittest.main()
+61
View File
@@ -0,0 +1,61 @@
import importlib.util
import tempfile
import unittest
from pathlib import Path
def load_tdd_guard():
module_path = Path(__file__).resolve().parent.parent / ".codex" / "hooks" / "tdd-guard.py"
spec = importlib.util.spec_from_file_location("tdd_guard", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class CppTddGuardTests(unittest.TestCase):
def test_cpp_production_file_without_related_test_is_blocked(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "include" / "fesa" / "Core" / "DofManager.hpp"
source.parent.mkdir(parents=True)
source.write_text("#pragma once\n", encoding="utf-8")
self.assertEqual(tdd_guard._guarded_paths([str(source)], root, root), ["DofManager"])
def test_cpp_production_file_with_module_test_is_allowed(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "include" / "fesa" / "Core" / "DofManager.hpp"
source.parent.mkdir(parents=True)
source.write_text("#pragma once\n", encoding="utf-8")
tests_dir = root / "tests"
tests_dir.mkdir()
(tests_dir / "test_core_module_includes.cpp").write_text("int main() { return 0; }\n", encoding="utf-8")
self.assertEqual(tdd_guard._guarded_paths([str(source)], root, root), [])
def test_cpp_production_file_with_basename_test_in_same_patch_is_allowed(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "src" / "Math" / "DenseMatrix.cpp"
source.parent.mkdir(parents=True)
source.write_text("void f() {}\n", encoding="utf-8")
test_path = root / "tests" / "test_dense_matrix.cpp"
self.assertEqual(tdd_guard._guarded_paths([str(source), str(test_path)], root, root), [])
def test_cmake_and_docs_are_exempt(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
self.assertEqual(
tdd_guard._guarded_paths(["CMakeLists.txt", "docs/ARCHITECTURE.md", "cmake/toolchain.cmake"], root, root),
[],
)
if __name__ == "__main__":
unittest.main()
+94
View File
@@ -0,0 +1,94 @@
import importlib.util
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
def load_validate_workspace():
module_path = Path(__file__).resolve().parent / "validate_workspace.py"
spec = importlib.util.spec_from_file_location("validate_workspace", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class ValidateWorkspaceTests(unittest.TestCase):
def test_env_commands_override_cmake_detection(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
with patch.dict(os.environ, {"HARNESS_VALIDATION_COMMANDS": "echo first\n echo second \n"}, clear=True):
self.assertEqual(validate_workspace.discover_commands(root), ["echo first", "echo second"])
def test_msvc_debug_cmake_commands_are_default_for_cmake_project(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
build_dir = root / "build" / "msvc-debug"
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(
validate_workspace.discover_commands(root),
[
f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64',
f'cmake --build "{build_dir}" --config Debug',
f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug',
],
)
def test_msvc_debug_configure_preset_is_preferred_when_present(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
(root / "CMakePresets.json").write_text(
"""
{
"version": 3,
"configurePresets": [
{
"name": "msvc-debug",
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/out/msvc-debug"
}
]
}
""",
encoding="utf-8",
)
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(
validate_workspace.discover_commands(root),
[
"cmake --preset msvc-debug",
f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug',
f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug',
],
)
def test_no_cmake_project_has_no_validation_commands(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(validate_workspace.discover_commands(root), [])
def test_common_cmake_install_path_is_prepended_when_cmake_is_not_on_path(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
common_bin = Path(tmp) / "CMake" / "bin"
common_bin.mkdir(parents=True)
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
(common_bin / "ctest.exe").write_text("", encoding="utf-8")
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
with patch.object(validate_workspace.shutil, "which", return_value=None):
env = validate_workspace.validation_environment({"PATH": "C:\\Windows\\System32"})
self.assertTrue(env["PATH"].startswith(str(common_bin)))
if __name__ == "__main__":
unittest.main()
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Run C++/MSVC Harness validation commands."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
DEFAULT_GENERATOR = "Visual Studio 17 2022"
DEFAULT_PLATFORM = "x64"
DEFAULT_CONFIG = "Debug"
DEFAULT_BUILD_DIR = "build/msvc-debug"
PRESET_NAME = "msvc-debug"
COMMON_CMAKE_BIN = Path(r"C:\Program Files\CMake\bin")
def load_env_commands() -> list[str]:
raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "")
return [line.strip() for line in raw.splitlines() if line.strip()]
def _cmake_config() -> tuple[str, str, str, Path]:
generator = os.environ.get("HARNESS_CMAKE_GENERATOR", DEFAULT_GENERATOR)
platform = os.environ.get("HARNESS_CMAKE_PLATFORM", DEFAULT_PLATFORM)
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
build_dir = Path(os.environ.get("HARNESS_BUILD_DIR", DEFAULT_BUILD_DIR))
return generator, platform, config, build_dir
def _read_presets(root: Path) -> dict:
presets_file = root / "CMakePresets.json"
if not presets_file.exists():
return {}
try:
return json.loads(presets_file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return {}
def _preset_binary_dir(root: Path, preset: dict) -> Path:
binary_dir = str(preset.get("binaryDir") or DEFAULT_BUILD_DIR)
binary_dir = binary_dir.replace("${sourceDir}", str(root))
binary_dir = binary_dir.replace("$sourceDir", str(root))
path = Path(binary_dir)
return path if path.is_absolute() else root / path
def load_preset_commands(root: Path) -> list[str]:
payload = _read_presets(root)
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
for preset in payload.get("configurePresets", []):
if isinstance(preset, dict) and preset.get("name") == PRESET_NAME:
build_dir = _preset_binary_dir(root, preset)
return [
f"cmake --preset {PRESET_NAME}",
f'cmake --build "{build_dir}" --config {config}',
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
]
return []
def load_cmake_commands(root: Path) -> list[str]:
if not (root / "CMakeLists.txt").exists():
return []
generator, platform, config, build_dir = _cmake_config()
if not build_dir.is_absolute():
build_dir = root / build_dir
return [
f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
f'cmake --build "{build_dir}" --config {config}',
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
]
def discover_commands(root: Path) -> list[str]:
env_commands = load_env_commands()
if env_commands:
return env_commands
preset_commands = load_preset_commands(root)
if preset_commands:
return preset_commands
return load_cmake_commands(root)
def run_command(command: str, root: Path) -> subprocess.CompletedProcess:
return subprocess.run(
command,
cwd=root,
shell=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=validation_environment(os.environ),
)
def validation_environment(base_env: os._Environ | dict[str, str]) -> dict[str, str]:
env = dict(base_env)
if shutil.which("cmake") is not None:
return env
cmake_exe = COMMON_CMAKE_BIN / "cmake.exe"
if not cmake_exe.exists():
return env
current_path = env.get("PATH", "")
paths = [part for part in current_path.split(os.pathsep) if part]
common_bin_text = str(COMMON_CMAKE_BIN)
if not any(part.lower() == common_bin_text.lower() for part in paths):
env["PATH"] = common_bin_text + (os.pathsep + current_path if current_path else "")
return env
def emit_stream(prefix: str, content: str, *, stream) -> None:
text = (content or "").strip()
if not text:
return
print(prefix, file=stream)
print(text, file=stream)
def main() -> int:
root = Path(__file__).resolve().parent.parent
commands = discover_commands(root)
if not commands:
print("No C++ validation commands configured.")
print("Add CMakeLists.txt or set HARNESS_VALIDATION_COMMANDS.")
return 0
for command in commands:
print(f"$ {command}")
result = run_command(command, root)
emit_stream("[stdout]", result.stdout, stream=sys.stdout)
emit_stream("[stderr]", result.stderr, stream=sys.stderr)
if result.returncode != 0:
print(f"Validation failed: {command}", file=sys.stderr)
return result.returncode
print("Validation succeeded.")
return 0
if __name__ == "__main__":
raise SystemExit(main())