diff --git a/.agents/skills/harness/SKILL.md b/.agents/skills/harness/SKILL.md new file mode 100644 index 0000000..49519ee --- /dev/null +++ b/.agents/skills/harness/SKILL.md @@ -0,0 +1,172 @@ +--- +name: harness +description: Use when planning agentic implementation phases, creating phases/index.json and self-contained step files, or running the Harness step executor. +--- + +# Harness Workflow + +이 프로젝트는 Harness 프레임워크를 사용한다. 아래 워크플로에 따라 작업한다. + +## A. 탐색 + +`AGENTS.md`와 `docs/` 하위 문서(PRD, ARCHITECTURE, ADR 등)를 읽고 프로젝트의 기획, +아키텍처, 설계 의도를 파악한다. 병렬 탐색이 실제로 유용하고 현재 세션에서 허용될 +때만 Codex subagent를 선택적으로 사용한다. + +## B. 논의 + +구현을 위해 구체화하거나 기술적으로 결정해야 할 사항이 있으면 사용자에게 한 번에 +하나씩 제시하고 논의한다. + +## C. Step 설계 + +사용자가 구현 계획 작성을 지시하면 여러 step으로 나뉜 초안을 작성해 피드백을 +요청한다. + +설계 원칙: + +1. **Scope 최소화** — 하나의 step에서 하나의 레이어 또는 모듈만 다룬다. 여러 + 모듈을 동시에 수정해야 하면 step을 쪼갠다. +2. **자기완결성** — 각 step 파일은 독립된 Codex 실행에서 사용된다. 외부 대화 + 참조를 금지하고 필요한 정보를 모두 파일 안에 적는다. +3. **사전 준비 강제** — 관련 문서와 이전 step에서 생성하거나 수정한 파일 경로를 + 명시한다. +4. **시그니처 수준 지시** — 함수와 클래스의 인터페이스를 제시하고 내부 구현은 + Codex 재량에 맡긴다. 멱등성, 보안, 데이터 무결성 같은 핵심 규칙은 명시한다. +5. **AC는 실행 가능한 command** — 추상적 조건 대신 실제 빌드와 테스트 command를 + 포함한다. +6. **주의사항은 구체적으로** — "X를 하지 마라. 이유: Y" 형식으로 적는다. +7. **네이밍** — step name은 핵심 작업을 표현하는 kebab-case slug로 정한다. + +## D. 파일 생성 + +사용자가 초안을 승인한 후에만 다음 파일을 생성한다. + +### D-1. `phases/index.json` + +여러 task를 관리하는 top-level 인덱스다. 이미 존재하면 `phases` 배열에 새 항목을 +추가한다. + +```json +{ + "phases": [ + { + "dir": "0-mvp", + "status": "pending" + } + ] +} +``` + +- `dir`: task 디렉터리명 +- `status`: `pending` | `completed` | `error` | `blocked` +- timestamp는 executor가 상태를 바꿀 때 기록하므로 생성 시 넣지 않는다. + +### D-2. `phases/{task-name}/index.json` + +```json +{ + "project": "<프로젝트명>", + "phase": "", + "steps": [ + { "step": 0, "name": "project-setup", "status": "pending" }, + { "step": 1, "name": "core-types", "status": "pending" }, + { "step": 2, "name": "api-layer", "status": "pending" } + ] +} +``` + +필드 규칙: + +- `project`: `AGENTS.md`에 정의된 프로젝트명 +- `phase`: task 이름이며 디렉터리명과 일치 +- `steps[].step`: 0부터 시작하는 순번 +- `steps[].name`: kebab-case slug +- `steps[].status`: 초기값 `pending` + +상태와 기록 주체: + +| 전이 | 기록 필드 | 기록 주체 | +|------|-----------|-----------| +| `completed` | `summary`, `completed_at` | Codex가 summary, executor가 timestamp | +| `error` | `error_message`, `failed_at` | Codex가 message, executor가 timestamp | +| `blocked` | `blocked_reason`, `blocked_at` | Codex가 reason, executor가 timestamp | + +`summary`에는 다음 step에 유용한 생성 파일과 핵심 결정을 한 줄로 적는다. +task `created_at`과 step `started_at`은 executor가 기록하므로 생성 시 넣지 않는다. + +### D-3. `phases/{task-name}/step{N}.md` + +````markdown +# Step {N}: {이름} + +## 읽어야 할 파일 + +먼저 아래 파일을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라: + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- 이전 step에서 생성하거나 수정한 파일 경로 + +이전 step의 코드를 꼼꼼히 읽고 설계 의도를 이해한 뒤 작업하라. + +## 작업 + +구체적인 구현 지시를 파일 경로, 클래스와 함수 시그니처, 로직 설명과 함께 적는다. +구현체는 Codex에 맡기되 설계 의도에서 벗어나면 안 되는 핵심 규칙은 명시한다. + +## Acceptance Criteria + +프로젝트 형식에 맞는 명령을 사용한다. `.harness/config.json`이 있으면 해당 preset, +solution, configuration, platform, test command를 우선한다. + +```powershell +# CMake +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --output-on-failure + +# 직접 MSBuild +MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64 +.\build\tests\Debug\MyProjectTests.exe +``` + +## 검증 절차 + +1. Acceptance Criteria command를 실행한다. +2. ARCHITECTURE 디렉터리 구조를 따르는지 확인한다. +3. ADR 기술 스택과 `AGENTS.md` CRITICAL 규칙을 확인한다. +4. 결과에 따라 task index의 해당 step을 갱신한다. + - 성공: `status`를 `completed`로 바꾸고 한 줄 `summary` 기록 + - 수정 3회 후 실패: `status`를 `error`로 바꾸고 `error_message` 기록 + - 사용자 개입 필요: `status`를 `blocked`로 바꾸고 `blocked_reason` 기록 후 중단 + +## 금지사항 + +- 이 step의 범위 밖 기능을 추가하지 마라. 이유: step의 독립성을 깨뜨린다. +- 기존 테스트를 깨뜨리지 마라. 이유: 이전 동작을 회귀시킨다. +```` + +## E. 실행 + +```bash +python scripts/execute.py {task-name} +python scripts/execute.py {task-name} --push +``` + +환경에서 Python 3 실행 명령이 `python3`이면 그 명령을 대신 사용한다. + +executor가 처리하는 작업: + +- `feat-{task-name}` 브랜치 생성 또는 checkout +- `AGENTS.md`와 `docs/*.md` guardrail 주입 +- 완료 step의 summary를 다음 prompt에 누적 +- 실패 시 최대 3회 재시도하며 이전 오류를 prompt에 전달 +- 코드 변경과 metadata를 분리해 commit +- `started_at`, `completed_at`, `failed_at`, `blocked_at` 기록 + +에러 복구: + +- `error`: 해당 status를 `pending`으로 바꾸고 `error_message`를 삭제한 뒤 재실행 +- `blocked`: 원인을 해결하고 status를 `pending`으로 바꾸고 `blocked_reason`을 삭제한 + 뒤 재실행 diff --git a/.agents/skills/review/SKILL.md b/.agents/skills/review/SKILL.md new file mode 100644 index 0000000..9f695da --- /dev/null +++ b/.agents/skills/review/SKILL.md @@ -0,0 +1,41 @@ +--- +name: review +description: Use when reviewing repository changes against AGENTS.md, architecture decisions, tests, and build requirements. +--- + +# Repository Review + +먼저 다음 문서를 읽는다. + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` + +사용자가 범위를 지정하지 않으면 현재 작업 트리의 변경을 리뷰한다. 관련 diff를 +확인하고 가능한 빌드와 테스트 command를 실제로 실행한다. + +## 체크리스트 + +1. MSVC toolset와 C++ 표준이 AGENTS.md/ADR과 일치하는가? +2. CMake 또는 MSBuild Debug/x64 빌드가 통과하는가? +3. CTest 또는 `.harness/config.json`의 명시적 test command가 통과하는가? +4. 새 C/C++ 소스와 헤더에 대응 테스트가 있는가? +5. CRITICAL 아키텍처 규칙과 public header 경계를 지키는가? + +## 출력 형식 + +실제 결함을 심각도순으로 먼저 제시한다. 각 finding에 파일과 줄 번호, 영향, +재현 또는 근거, 구체적인 수정 방안을 포함한다. + +그 뒤 다음 표를 제공한다. + +| 항목 | 결과 | 비고 | +|------|------|------| +| 아키텍처 준수 | ✅/❌/미검증 | 상세 | +| 기술 스택 준수 | ✅/❌/미검증 | 상세 | +| 테스트 존재 | ✅/❌/미검증 | 상세 | +| CRITICAL 규칙 | ✅/❌/미검증 | 상세 | +| 빌드 가능 | ✅/❌/미검증 | 상세 | + +실행할 수 없는 검사는 성공으로 추정하지 말고 `미검증`과 이유를 적는다. finding이 +없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다. diff --git a/.codex/agents/build-test-executor-agent.toml b/.codex/agents/build-test-executor-agent.toml index 2da5239..862431c 100644 --- a/.codex/agents/build-test-executor-agent.toml +++ b/.codex/agents/build-test-executor-agent.toml @@ -10,7 +10,7 @@ Mission: - Run build and test validation only after Implementation Agent work. - Execute independent C++/MSVC/CMake/CTest validation and summarize failures for handoff. - Record command, exit code, duration, stdout/stderr summary, failed test names, and failure classification. -- Keep the output aligned with AGENTS.md, docs/SOLVER_AGENT_DESIGN.md, scripts/validate_workspace.py, and the implementation plan/report. +- Keep the output aligned with AGENTS.md, docs/HARNESS_WORKFLOW.md, docs/SOLVER_AGENT_DESIGN.md, `.harness/config.json` when present, and the implementation plan/report. Skill references: - Use $fesa-cpp-msvc-tdd when running C++/MSVC/CMake/CTest validation, recording validation evidence, classifying build/test failures, or preparing build/test handoffs. @@ -32,23 +32,26 @@ Input priorities: 2. Implementation Agent report. 3. docs/implementation-plans/-implementation-plan.md. 4. AGENTS.md and docs/SOLVER_AGENT_DESIGN.md. -5. scripts/validate_workspace.py. -6. CMakePresets.json, CMakeLists.txt, CMake files, and CTest metadata when present. +5. `.harness/config.json` when present. +6. CMakePresets.json, CMakeLists.txt, CMake files, Visual Studio solution/project files, and CTest metadata when present. 7. Related docs/reference-models/-reference-models.md when present. 8. Stored reference artifacts when present, read-only. Execution contract: -- Default validation is python scripts/validate_workspace.py. -- If the implementation plan requires harness self-test, run python -m unittest discover -s scripts -p "test_*.py" first. -- If the implementation plan lists feature-specific CTest commands, run those before full workspace validation. -- Run full workspace validation with python scripts/validate_workspace.py last. -- scripts/validate_workspace.py resolves HARNESS_VALIDATION_COMMANDS, CMakePresets.json msvc-debug, or CMake/MSVC x64 Debug commands. -- The default CMake/MSVC x64 Debug commands are: - 1. cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64 - 2. cmake --build build/msvc-debug --config Debug - 3. ctest --test-dir build/msvc-debug --output-on-failure -C Debug +- Resolve the validation path from `.harness/config.json` first, then Harness project auto detection. +- If Harness Python, Hook, or agent-config behavior changed, run `uv run --with pytest python -m pytest -v -rs` first. +- Configure and build before running feature-specific and full tests. +- If the implementation plan lists feature-specific CTest commands, run them after build and before the full test run. +- For a non-preset CMake project, run: + 1. cmake -S . -B .harness/build -A x64 + 2. cmake --build .harness/build --config Debug + 3. ctest --test-dir .harness/build -C Debug -R --output-on-failure when specified + 4. ctest --test-dir .harness/build -C Debug --show-only=json-v1 + 5. ctest --test-dir .harness/build -C Debug --output-on-failure +- If `.harness/config.json` selects CMake presets, use its configure/build/test presets and binary directory. +- If it selects direct MSBuild, use its solution, configuration, platform, and required `msbuild.testCommand`. - Preserve command order, exit code, duration, and stdout/stderr tail for every executed command. -- For no-CMake workspaces, record the scripts/validate_workspace.py informational success path instead of treating it as a failure. +- Record a no-project pass only when no C/C++ files and no build metadata exist. C/C++ files without build metadata are an error. - Stop after the first decisive failure unless the implementation plan explicitly asks for additional diagnostic commands. Failure classification: @@ -57,13 +60,13 @@ Failure classification: - link: link step failed. - test: CTest or unit/integration tests failed. - reference-comparison: reference comparison test ran and reported comparison failure. -- harness: Python harness self-test or validation script failed. +- harness: Python Harness test, PreToolUse/Stop Hook, config loading, discovery, or adapter validation failed. - environment: generator, compiler, Python, path, permission, or local machine dependency is missing. - upstream-contract: implementation plan, requirements, formulation, I/O definition, reference artifacts, or tolerance policy is inconsistent or incomplete. Required Build/Test Report sections: 1. Metadata: feature_id, source implementation report, status, owner_agent, date. -2. Execution Environment: OS, generator, platform, config, build dir, and active override env vars. +2. Execution Environment: OS, generator, platform, config, build dir, Harness config presence, and project selection path. 3. Command Log Summary: command, exit code, duration, stdout/stderr tail. 4. Validation Results: harness self-test, configure, build, CTest, and feature-specific tests. 5. Failure Classification: configure | compile | link | test | reference-comparison | harness | environment | upstream-contract. diff --git a/.codex/agents/correction-agent.toml b/.codex/agents/correction-agent.toml index a322a03..e5442d0 100644 --- a/.codex/agents/correction-agent.toml +++ b/.codex/agents/correction-agent.toml @@ -48,8 +48,9 @@ Execution contract: - MINIMAL FIX: modify only implementation-owned source, header, test, or CMake files needed to fix the classified failure. - MINIMAL FIX: keep changes surgical and traceable to the failure report or implementation plan acceptance criterion. - VERIFY: rerun the targeted command that reproduced the failure first. -- VERIFY: run python scripts/validate_workspace.py after the targeted command. -- VERIFY: run python -m unittest discover -s scripts -p "test_*.py" when harness, hook, or agent config behavior is involved. +- VERIFY: run the full MSVC build/test commands resolved from `.harness/config.json` or Harness auto detection after the targeted command. +- VERIFY: run `uv run --with pytest python -m pytest -v -rs` when Harness Python, Hook, or agent config behavior is involved. +- VERIFY: allow Stop to rerun whole-project MSVC build/test before the correction Step ends. - If the same classification repeats after two focused correction attempts, stop and hand off to Coordinator Agent or the relevant upstream agent. - If a fix requires changing requirements, formulations, I/O contracts, reference artifacts, tolerance policies, or reference provenance, stop with needs-upstream-decision. - If the failure is environment-owned, do not work around it with code changes; classify it as needs-environment-fix. @@ -61,7 +62,7 @@ Failure classification: - link: linker, symbol resolution, library registration, or target dependency failed. - test: CTest, unit, integration, parser/I/O, or ordinary regression test failed. - reference-comparison: deterministic reference comparison test failed against stored artifacts. -- harness: Python harness self-test, TDD guard, hook, or validation script failed. +- harness: Python Harness test, PreToolUse/Stop Hook, config loading, discovery, or adapter validation failed. - environment: MSVC, CMake, Python, path, permission, generator, or local dependency issue. - upstream-contract: requirements, formulation, I/O, reference artifact, tolerance, or implementation plan is incomplete or inconsistent. @@ -70,7 +71,7 @@ Required Correction Report sections: 2. Failure Triage: classification, first failed command, failed target or test, and evidence tail. 3. Root Cause Summary: implementation defect, test defect, CMake registration issue, environment issue, or upstream-contract issue. 4. Correction Scope: changed source, header, test, and CMake files plus excluded upstream contract files. -5. Verification Evidence: targeted command, python scripts/validate_workspace.py, and Python harness self-test when relevant. +5. Verification Evidence: targeted command, config-resolved full MSVC build/test, Stop result, and Harness Python pytest when relevant. 6. Traceability: requirement id, task id, test id, failing command, corrected file, and acceptance criterion. 7. Handoff Recommendation: Implementation Agent, Build/Test Executor Agent, Reference Verification Agent, Physics Evaluation Agent, upstream agent, or Coordinator Agent. 8. Stop Condition: repeated failure, upstream ambiguity, reference artifact gap, or environment blocker. diff --git a/.codex/agents/implementation-agent.toml b/.codex/agents/implementation-agent.toml index c7ed6b0..285d4a1 100644 --- a/.codex/agents/implementation-agent.toml +++ b/.codex/agents/implementation-agent.toml @@ -43,7 +43,9 @@ Execution contract: - RED: write the planned C++ unit, integration, parser/I/O, or reference-comparison test first. - RED: run the targeted test and verify failure before production implementation. - GREEN: implement the minimum code needed for the planned task and acceptance criterion. -- VERIFY: run the targeted CTest command, then the workspace validation commands. +- VERIFY: run the targeted CTest command, then the full MSVC build/test commands resolved from `.harness/config.json` or the Harness defaults. +- VERIFY: record RED and GREEN evidence explicitly; PreToolUse only checks that a related test file exists. +- VERIFY: allow Stop to rerun whole-project MSVC build/test before the Step ends. - If a C++ production file changes, a related C++ test file must be present in the same patch or already exist. - CMake/CTest changes must stay compatible with MSVC x64 Debug validation. - Abaqus reference CSV files are read-only verification inputs. @@ -69,15 +71,19 @@ Required Implementation Report sections: 2. Implemented Scope: completed task ids, skipped task ids, and reason. 3. Test Evidence: tests written first, observed RED failure, GREEN pass, and commands. 4. Code Changes: source, header, test, and CMake/CTest change summary. -5. Validation Evidence: ctest -C Debug, python scripts/validate_workspace.py, and python -m unittest discover -s scripts -p "test_*.py" when relevant. +5. Validation Evidence: targeted CTest, config-resolved full MSVC build/test, Stop result, and `uv run --with pytest python -m pytest -v -rs` when Harness Python behavior is relevant. 6. Traceability: requirement id, task id, test id, and acceptance criterion. 7. Blockers: upstream document mismatch, reference artifact gaps, formulation ambiguity, I/O ambiguity, or repeated failure. 8. Downstream Handoff: Build/Test Executor Agent, Correction Agent, and Reference Verification Agent. Validation commands: -- python -m unittest discover -s scripts -p "test_*.py" -- python scripts/validate_workspace.py -- ctest -C Debug -R +- cmake -S . -B .harness/build -A x64 +- cmake --build .harness/build --config Debug +- ctest --test-dir .harness/build -C Debug -R --output-on-failure +- ctest --test-dir .harness/build -C Debug --show-only=json-v1 +- ctest --test-dir .harness/build -C Debug --output-on-failure +- Use configured CMake presets or direct MSBuild commands instead when `.harness/config.json` selects them. +- Run `uv run --with pytest python -m pytest -v -rs` when Harness Python, Hook, or agent-config behavior changes. Status rules: - in-progress: implementation is underway. diff --git a/.codex/agents/implementation-planning-agent.toml b/.codex/agents/implementation-planning-agent.toml index e408407..b791a48 100644 --- a/.codex/agents/implementation-planning-agent.toml +++ b/.codex/agents/implementation-planning-agent.toml @@ -55,11 +55,11 @@ Required Implementation Plan sections: 3. Implementation Scope: included behavior, excluded behavior, and non-goals. 4. Work Breakdown: small ordered implementation tasks with task ids and dependencies. 5. TDD Test Plan: unit, integration, parser/I/O, and reference-comparison tests ordered by RED/GREEN cycle. -6. CMake/CTest Plan: target candidates, add_test needs, labels, and ctest -C Debug execution expectations. +6. CMake/CTest Plan: target candidates, add_test needs, labels, and `.harness/config.json` or default `.harness/build` execution expectations. 7. Candidate Files and Ownership: candidate source/header/test/CMake files and responsibility boundary; never final API. 8. Data Flow Contract: Abaqus .inp input, internal model, solver results.h5, Abaqus reference CSV files under reference//, and FESA HDF5-to-reference-CSV comparison flow. 9. Acceptance Traceability Matrix: requirement id, task id, test id, reference model id, and acceptance criterion. -10. Validation Commands: python -m unittest discover -s scripts -p \"test_*.py\", python scripts/validate_workspace.py, and feature-specific CTest commands. +10. Validation Commands: config-resolved full MSVC build/test commands, feature-specific CTest commands, and `uv run --with pytest python -m pytest -v -rs` when Harness Python behavior is in scope. 11. Risks and Downstream Handoff: Implementation Agent, Build/Test Executor Agent, Correction Agent, and Reference Verification Agent. 12. Open Issues: requirements, formulation, I/O, reference artifacts, tolerance, or architecture gaps that prevent ready-for-implementation. diff --git a/.codex/agents/release-agent.toml b/.codex/agents/release-agent.toml index 15d5dbc..7fd20c7 100644 --- a/.codex/agents/release-agent.toml +++ b/.codex/agents/release-agent.toml @@ -62,7 +62,7 @@ Required Release Report sections: 2. Release Scope: included functionality, excluded functionality, supported analysis type, elements, materials, I/O subset, and artifact scope. 3. Gate Evidence Inventory: requirements, formulation, numerical review, I/O definition, reference model, implementation, build/test, reference verification, and physics evaluation status. 4. Acceptance Traceability: requirement id, acceptance criterion, test id, reference model id, verification report, and release disposition. -5. Validation Evidence: python scripts/validate_workspace.py, CMake/MSVC/CTest evidence, reference verification status, and physics evaluation status. +5. Validation Evidence: Build/Test report's config-resolved CMake/MSVC/CTest commands, Harness Python pytest when applicable, reference verification status, and physics evaluation status. 6. Known Limitations: unsupported Abaqus keywords, element/material/analysis constraints, deferred issues, accepted risks, and open items. 7. Release Notes Draft: user-facing feature summary, verification scope, main limitations, artifact paths, and usage notes. 8. Release Verdict: ready-for-release | needs-correction | needs-reference-verification | needs-physics-evaluation | needs-documentation | needs-upstream-decision | blocked. diff --git a/.codex/config.toml b/.codex/config.toml index b75aa36..151b8b6 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,4 +1,4 @@ #:schema https://developers.openai.com/codex/config-schema.json [features] -codex_hooks = true +hooks = true diff --git a/.codex/hooks.json b/.codex/hooks.json index 0229d62..598631c 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -1,25 +1,29 @@ { + "description": "Harness TDD, command safety, and MSVC C/C++ validation hooks.", "hooks": { "PreToolUse": [ { - "matcher": "^Bash$", + "matcher": "Bash|shell_command|PowerShell|apply_patch|Edit|MultiEdit|Write", "hooks": [ { "type": "command", - "command": "python -c \"import pathlib, runpy, subprocess; root = pathlib.Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()); runpy.run_path(str(root / '.codex' / 'hooks' / 'pre_commit_checks.py'), run_name='__main__')\"", - "timeout": 600, - "statusMessage": "Running pre-commit checks" + "command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"", + "commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"", + "timeout": 30, + "statusMessage": "Checking Harness policies" } ] - }, + } + ], + "Stop": [ { - "matcher": "^(apply_patch|Edit|Write)$", "hooks": [ { "type": "command", - "command": "python -c \"import pathlib, runpy, subprocess; root = pathlib.Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()); runpy.run_path(str(root / '.codex' / 'hooks' / 'tdd-guard.py'), run_name='__main__')\"", - "timeout": 30, - "statusMessage": "Checking TDD guard" + "command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"", + "commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"", + "timeout": 1800, + "statusMessage": "Running MSVC build and tests" } ] } diff --git a/.codex/hooks/pre_commit_checks.py b/.codex/hooks/pre_commit_checks.py deleted file mode 100644 index 92dd51d..0000000 --- a/.codex/hooks/pre_commit_checks.py +++ /dev/null @@ -1,89 +0,0 @@ -import json -import re -import subprocess -import sys -from pathlib import Path - - -def _repo_root(cwd: Path) -> Path: - try: - root = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], - cwd=cwd, - text=True, - stderr=subprocess.DEVNULL, - ).strip() - except (subprocess.CalledProcessError, FileNotFoundError): - return cwd - return Path(root) - - -def _is_git_commit(command: str) -> bool: - 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: - print( - json.dumps( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": reason, - } - } - ) - ) - - -def _tail(text: str, limit: int = 1200) -> str: - text = text.strip() - if len(text) <= limit: - return text - return text[-limit:] - - -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"], - ] - - -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"{label} failed:\n{details}" - return f"{label} failed with exit code {result.returncode}." - - return None - - -def main() -> int: - try: - payload = json.load(sys.stdin) - except json.JSONDecodeError: - return 0 - - command = payload.get("tool_input", {}).get("command", "") - if not isinstance(command, str) or not _is_git_commit(command): - return 0 - - cwd = Path(payload.get("cwd") or Path.cwd()) - root = _repo_root(cwd) - failure = _run_checks(root) - if failure: - _deny(f"PRE-COMMIT CHECKS: {failure}") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.codex/hooks/tdd-guard.py b/.codex/hooks/tdd-guard.py deleted file mode 100644 index 0e4cba7..0000000 --- a/.codex/hooks/tdd-guard.py +++ /dev/null @@ -1,205 +0,0 @@ -import json -import subprocess -import sys -from pathlib import Path - - -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: - try: - root = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], - cwd=cwd, - text=True, - stderr=subprocess.DEVNULL, - ).strip() - except (subprocess.CalledProcessError, FileNotFoundError): - return cwd - return Path(root) - - -def _extract_patch_paths(command: str) -> list[str]: - prefixes = ( - "*** Add File: ", - "*** Update File: ", - "*** Delete File: ", - "*** Move to: ", - ) - paths: list[str] = [] - for raw_line in command.splitlines(): - line = raw_line.strip() - for prefix in prefixes: - if line.startswith(prefix): - paths.append(line[len(prefix) :].strip()) - break - return paths - - -def _touched_paths(payload: dict) -> list[str]: - tool_input = payload.get("tool_input", {}) - if not isinstance(tool_input, dict): - return [] - - file_path = tool_input.get("file_path") - if isinstance(file_path, str) and file_path: - return [file_path] - - command = tool_input.get("command") - if isinstance(command, str): - return _extract_patch_paths(command) - - return [] - - -def _normalize(path_text: str) -> str: - return path_text.replace("\\", "/").lower() - - -def _is_test_path(path_text: str) -> bool: - normalized = _normalize(path_text) - name = normalized.rsplit("/", 1)[-1] - path = Path(path_text) - return ( - "/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 - ) 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: - normalized = _normalize(path_text) - path = Path(path_text) - name = path.name.lower() - - if name == "cmakelists.txt": - return True - if _is_test_path(path_text): - return True - if path.suffix.lower() in CONFIG_SUFFIXES: - return True - if "/cmake/" in normalized: - return True - - return False - - -def _resolve_path(path_text: str, cwd: Path) -> Path: - path = Path(path_text) - if path.is_absolute(): - return path - return (cwd / path).resolve() - - -def _base_name(path: Path) -> str: - 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 _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 - - path = _resolve_path(path_text, cwd) - if path.suffix.lower() not in SOURCE_SUFFIXES: - continue - if not _has_related_test(path, candidate_tests): - missing_tests.append(_base_name(path)) - - return missing_tests - - -def main() -> int: - try: - payload = json.load(sys.stdin) - except json.JSONDecodeError: - return 0 - - cwd = Path(payload.get("cwd") or Path.cwd()) - root = _repo_root(cwd) - missing_tests = _guarded_paths(_touched_paths(payload), cwd, root) - if not missing_tests: - return 0 - - names = ", ".join(sorted(set(missing_tests))) - print( - json.dumps( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": ( - "TDD GUARD: missing test file for " - f"{names}. Write or add the test first." - ), - } - } - ) - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.codex/skills/fesa-cpp-msvc-tdd/SKILL.md b/.codex/skills/fesa-cpp-msvc-tdd/SKILL.md index 1dc6d9b..31f6eb5 100644 --- a/.codex/skills/fesa-cpp-msvc-tdd/SKILL.md +++ b/.codex/skills/fesa-cpp-msvc-tdd/SKILL.md @@ -26,10 +26,12 @@ Read these first: 3. RED: write the planned unit, integration, parser/I/O, or reference-comparison test first. 4. RED: run the targeted test and verify the expected failure before production code. 5. GREEN: implement the minimum C++17/MSVC-compatible code needed for the task. -6. VERIFY: run the targeted command, then `python scripts/validate_workspace.py`. +6. VERIFY: run the targeted command, then the full MSVC build/test commands resolved from `.harness/config.json` or the Harness defaults. 7. For C++ production changes, require a related C++ test file in the same patch or already present. -8. For failure triage, classify as `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`. -9. Fix implementation-owned failures only and keep changes traceable to the implementation plan. +8. Treat PreToolUse as a test-file-existence guardrail, not proof that RED was observed. Record the RED and GREEN commands and results in the implementation report. +9. Let Stop perform the final whole-project MSVC build/test before the Step ends. +10. For failure triage, classify as `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`. +11. Fix implementation-owned failures only and keep changes traceable to the implementation plan. ## Output Contract @@ -43,17 +45,19 @@ Produce one of these, depending on role: Required validation commands: ```powershell -python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_workspace.py -ctest -C Debug -R +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug -R --output-on-failure +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure ``` -Default MSVC path: +Use configured CMake presets or direct MSBuild commands instead when +`.harness/config.json` selects them. For Harness Python, Hook, or agent-config +changes, also run: ```powershell -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 +uv run --with pytest python -m pytest -v -rs ``` ## Boundaries @@ -73,6 +77,7 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug - Every `must` requirement maps to at least one task and one test. - Each test has a clear RED condition, GREEN condition, linked task, and command. - CMake/CTest plans remain compatible with MSVC x64 Debug validation. +- Stop validation is green for the whole discovered C/C++ project; a no-project pass is valid only when no C/C++ files and no build metadata exist. - Build/test reports record command, exit code, duration, stdout/stderr tail, and failure classification. - Correction attempts stop when repeated failure indicates upstream contract ambiguity. diff --git a/.codex/skills/harness-review/SKILL.md b/.codex/skills/harness-review/SKILL.md deleted file mode 100644 index 498d394..0000000 --- a/.codex/skills/harness-review/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: harness-review -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, 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, 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. - -## Checklist - -| Item | Question | -| --- | --- | -| 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 `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 - -If there are findings, list them first in severity order with file and line references when possible. Then include this table: - -| 항목 | 결과 | 비고 | -| --- | --- | --- | -| 아키텍처 준수 | PASS/FAIL | {상세} | -| 기술 스택 준수 | PASS/FAIL | {상세} | -| 테스트 존재 | PASS/FAIL | {상세} | -| TDD Guard | PASS/FAIL | {상세} | -| CRITICAL 규칙 | PASS/FAIL | {상세} | -| 빌드/검증 가능 | PASS/FAIL | {상세} | - -When there are no findings, say that clearly, then mention any commands not run or remaining risk. diff --git a/.codex/skills/harness-review/agents/openai.yaml b/.codex/skills/harness-review/agents/openai.yaml deleted file mode 100644 index 9af296a..0000000 --- a/.codex/skills/harness-review/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Harness Review" - short_description: "Review Harness changes safely" - default_prompt: "Use $harness-review to review Harness repository changes." diff --git a/.codex/skills/harness-workflow/SKILL.md b/.codex/skills/harness-workflow/SKILL.md deleted file mode 100644 index 061b2e3..0000000 --- a/.codex/skills/harness-workflow/SKILL.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -name: harness-workflow -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 every step grounded in repository docs, C++/MSVC constraints, TDD, and executable acceptance criteria. - -## Workflow - -1. Read `AGENTS.md` and relevant files under `docs/`, especially `docs/PRD.md`, `docs/ARCHITECTURE.md`, and `docs/ADR.md`. -2. Discuss unresolved product or technical decisions with the user before writing phase files. -3. When the user asks for an implementation plan, draft steps and get approval before creating files. -4. Create or update `phases/index.json`, `phases/{task-name}/index.json`, and one `phases/{task-name}/stepN.md` per step. -5. Run the phase with `python scripts/execute.py {task-name}` when asked to execute it. Use `--push` only when the user asks to push. - -## Step Design Rules - -- Scope each step to one layer or module. Split steps when multiple modules would otherwise change together. -- 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, 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 - -Create or update `phases/index.json`: - -```json -{ - "phases": [ - { - "dir": "0-mvp", - "status": "pending" - } - ] -} -``` - -Create `phases/{task-name}/index.json`: - -```json -{ - "project": "FESA Harness", - "phase": "", - "steps": [ - { "step": 0, "name": "project-setup", "status": "pending", "allowed_paths": ["CMakeLists.txt", "tests/"] }, - { "step": 1, "name": "core-types", "status": "pending", "allowed_paths": ["src/fesa/core/", "tests/unit/"] }, - { "step": 2, "name": "validation-path", "status": "pending", "allowed_paths": ["scripts/", "docs/"] } - ] -} -``` - -Rules: - -- `project` comes from `AGENTS.md`. -- `phase` matches the task directory name. -- `steps[].step` starts at `0`. -- Initial status is always `pending`. -- Each step must declare non-empty `allowed_paths` using repository-relative paths, directory prefixes, or glob patterns. -- Do not add timestamps when creating files. `scripts/execute.py` records `created_at`, `started_at`, `completed_at`, `failed_at`, and `blocked_at`. - -## Step Template - -```markdown -# Step {N}: {name} - -## 읽어야 할 파일 - -먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라: - -- `/AGENTS.md` -- `/docs/ARCHITECTURE.md` -- `/docs/ADR.md` -- {previously created or modified files} - -이전 step에서 만들어진 코드를 꼼꼼히 읽고, 설계 의도를 이해한 뒤 작업하라. - -## 작업 - -{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 -python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_workspace.py -``` - -## 검증 절차 - -1. 위 AC 커맨드를 실행한다. -2. 아키텍처 체크리스트를 확인한다: - - ARCHITECTURE.md 디렉토리 구조를 따르는가? - - ADR 기술 스택을 벗어나지 않았는가? - - AGENTS.md CRITICAL 규칙을 위반하지 않았는가? - - C++ 변경에는 관련 테스트가 존재하는가? -3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다: - - 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"` - - 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"` - - 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단 - -## 금지사항 - -- JavaScript/TypeScript/npm fallback을 추가하지 마라. Reason: 이 Harness는 C++/MSVC 전용이다. -- 기존 테스트를 깨뜨리지 마라. -``` - -## Execution And Recovery - -Run: - -```bash -python scripts/execute.py {task-name} -python scripts/execute.py {task-name} --push -``` - -`scripts/execute.py` creates or checks out `codex/{task-name}`, refuses dirty worktrees, requires per-step `allowed_paths`, stages only explicit allowed paths and runner housekeeping files, validates before every runner-created commit, injects `AGENTS.md` and `docs/*.md` into each prompt, carries completed step summaries forward, retries failed steps up to three times, and records timestamps. - -If a step is `error`, set it back to `pending` and remove `error_message` after fixing the cause. If a step is `blocked`, resolve `blocked_reason`, set it back to `pending`, remove `blocked_reason`, and rerun. diff --git a/.codex/skills/harness-workflow/agents/openai.yaml b/.codex/skills/harness-workflow/agents/openai.yaml deleted file mode 100644 index 3a671bf..0000000 --- a/.codex/skills/harness-workflow/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Harness Workflow" - short_description: "Plan staged Harness workflow steps" - default_prompt: "Use $harness-workflow to plan Harness phases and step files." diff --git a/.gitignore b/.gitignore index ab001a0..594a6a8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ Testing/ __pycache__/ *.pyc +# local Harness configuration and build outputs +.harness/config.json +.harness/build/ + # phase execution outputs phases/**/phase*-output.json phases/**/step*-output.json diff --git a/.harness/config.example.json b/.harness/config.example.json new file mode 100644 index 0000000..51d1846 --- /dev/null +++ b/.harness/config.example.json @@ -0,0 +1,16 @@ +{ + "version": 1, + "projectType": "auto", + "tdd": { + "testRoots": [ + "tests", + "test" + ], + "testPatterns": [ + "{stem}_test.cpp", + "{stem}_tests.cpp", + "test_{stem}.cpp", + "{stem}.test.cpp" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index 55f7fc9..234e627 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ ## 프로젝트 정체성 - FESA는 유한요소법 기반 구조해석 솔버 개발 프로젝트이다. -- Harness는 솔버 자체가 아니라 요구조건, TDD, phase 실행, 검증을 통제하는 개발 운영 인프라이다. +- Harness는 솔버 자체가 아니라 요구조건, TDD, phase 실행, 검증을 통제하는 개발 운영 인프라이다. 전체 실행 흐름은 `docs/HARNESS_WORKFLOW.md`, 설치와 설정은 `docs/HARNESS.md`를 따른다. - 문서와 구현은 full Abaqus compatibility를 주장하지 않는다. 기능별로 승인된 Abaqus keyword subset만 지원한다. - 공식 solver output은 HDF5 `results.h5`이다. - reference 결과는 FESA와 같은 Abaqus `.inp` 모델을 Abaqus로 해석해 생성한 CSV 파일이다. @@ -19,12 +19,12 @@ - CSV는 FESA 공식 output이 아니며, FESA HDF5에서 추출한 deterministic CSV view는 비교 디버깅/검토용 보조 artifact로만 둔다. ## 아키텍처 규칙 -- CRITICAL: 기본 검증 경로는 `python scripts/validate_workspace.py`이다. - CRITICAL: C++ 빌드는 CMake/MSVC/x64/Debug 기준으로 검증한다. - CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다. - CRITICAL: C++ production file을 바꿀 때는 관련 C++ test file이 있어야 한다. - CRITICAL: Abaqus reference artifact 생성, 수정, 복원은 명시적으로 요청된 phase에서만 수행한다. -- CRITICAL: `harness-workflow` 스킬은 사용자가 명시적으로 허용하기 전까지 사용하지 않는다. +- CRITICAL: public header와 implementation 의존성 방향을 역전하지 말 것 +- CRITICAL: 사람 개발자가 검토하기 쉽도록 코드를 작성할것. 그리고 주석도 필수로 작성할 것. - Domain은 입력 파일에서 생성된 전체 모델 정의를 소유하고, 파싱 이후 가능한 한 불변으로 취급한다. - AnalysisModel은 현재 step에서 활성화된 elements, loads, boundary conditions, properties/materials의 view를 제공하며 Domain을 복사하지 않는다. - DofManager는 node별 자유도 정의, constrained/free mapping, equation numbering, sparse pattern ownership을 전담한다. Node 또는 Element 내부에 equation id를 분산 저장하지 않는다. @@ -32,16 +32,16 @@ - MKL, TBB, HDF5 API는 solver core에 직접 노출하지 않는다. `LinearSolver`, `ParallelFor`, `ResultsWriter`, `Vector`, `Matrix`, `SparseMatrix` adapter 경계 뒤에 둔다. - Codex custom agent의 `model_reasoning_effort` 기본값은 `extra high`로 둔다. - Harness runner는 `scripts/execute.py`에 둔다. -- `scripts/execute.py`는 `codex/` branch prefix만 사용한다. -- `scripts/execute.py` 실행 전 worktree는 clean 상태여야 한다. -- 각 phase step은 non-empty `allowed_paths`를 선언해야 한다. -- runner는 explicit allowed path와 runner housekeeping file만 stage하며 broad staging을 사용하지 않는다. -- runner가 만드는 모든 commit 전에는 Harness Python self-test와 `python scripts/validate_workspace.py`가 통과해야 한다. -- Codex hook 정책은 `.codex/hooks/`에 둔다. +- `scripts/execute.py`는 `feat-` branch prefix를 사용한다. +- runner는 `git add -A`로 변경사항을 stage하므로 실행 전 clean worktree 또는 별도 Git worktree를 사용한다. +- Hook 연결은 `.codex/hooks.json`, 구현은 `scripts/hooks/`와 `scripts/msvc_harness/`에 둔다. +- PreToolUse는 위험 명령과 C++ production file의 대응 테스트 존재 여부를 검사하는 guardrail이며 RED 실행을 증명하지 않는다. +- Stop은 `.harness/config.json` 또는 자동 감지 결과에 따라 MSVC build와 test를 모두 검증한다. - Generated phase execution outputs remain ignored under `phases/**/step*-output.json`. ## 개발 프로세스 - TDD를 기본으로 한다. 구현은 `RED -> GREEN -> VERIFY` 순서를 따른다. +- CRITICAL: 빌드 경고를 새로 추가하지 말 것. - 기능 개발은 다음 gate를 순서대로 통과해야 한다. 1. 요구조건 분석 2. 연구자료 조사 @@ -54,10 +54,7 @@ 9. reference comparison 10. physics sanity 11. release readiness -- 커밋 전 hook은 Harness Python self-test와 workspace validation을 실행해야 한다. -- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`. -- Codex는 작업 완료 후 검증을 마치면 즉시 변경사항을 commit하고 push한다. -- 계획이 필요한 장기 작업은 Harness phase로 나누고, 각 step은 독립 실행 가능해야 한다. +- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`. ## Agent/Skill Workflow | 개발 과정 | Agent | Skill | 산출물 | @@ -76,22 +73,35 @@ | 배포 준비 | `release-agent` | `fesa-release-readiness` | `docs/releases/-release.md` | ## 명령어 -```bash -python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_workspace.py -python scripts/execute.py -python scripts/execute.py --push + +### Harness Python 검증 + +```powershell +uv run --with pytest python -m pytest -v -rs ``` -## MSVC 검증 기본값 -- Generator: `Visual Studio 17 2022` -- Platform: `x64` -- Config: `Debug` -- Build directory: `build/msvc-debug` +### Phase 실행 -Override variables: -- `HARNESS_VALIDATION_COMMANDS` -- `HARNESS_CMAKE_GENERATOR` -- `HARNESS_CMAKE_PLATFORM` -- `HARNESS_CMAKE_CONFIG` -- `HARNESS_BUILD_DIR` +```powershell +python scripts/execute.py +python scripts/execute.py --push +``` + +### CMake/CTest 프로젝트 + +```powershell +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure +``` + +### Visual Studio solution 프로젝트 + +```powershell +MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64 +.\build\tests\Debug\MyProjectTests.exe +``` + +실제 solution, preset, 테스트 명령은 `.harness/config.json`과 프로젝트 문서를 +우선한다. diff --git a/docs/ADR.md b/docs/ADR.md index a9640e5..e25f630 100644 --- a/docs/ADR.md +++ b/docs/ADR.md @@ -6,18 +6,18 @@ FESA의 architecture decision은 solver correctness, verification traceability, --- ### ADR-001: FESA는 구조해석 솔버 프로젝트이고 Harness는 운영 인프라로 둔다 -**결정**: 저장소의 주 목적은 유한요소법 기반 구조해석 솔버 개발이다. Harness scaffold는 phase execution, TDD guard, commit validation, workspace validation을 제공하는 보조 계층으로 유지한다. +**결정**: 저장소의 주 목적은 유한요소법 기반 구조해석 솔버 개발이다. Harness는 승인된 Step 계획, 독립 세션 실행, PreToolUse guardrail, Stop MSVC build/test 검증을 제공하는 보조 계층으로 유지한다. **이유**: 기존 문서가 Harness 중심이면 agent가 solver architecture, FEM verification, Abaqus/HDF5 계약보다 운영 스크립트에 과도하게 맞춰 행동한다. -**트레이드오프**: Harness 문서의 비중은 낮아지지만, 검증 명령과 hook 정책은 계속 필수 운영 규칙으로 유지한다. +**트레이드오프**: Harness 문서의 비중은 낮아지지만, `docs/HARNESS_WORKFLOW.md`의 실행 계약과 `.codex/hooks.json`의 검증 정책은 계속 필수 운영 규칙으로 유지한다. ### ADR-002: C++17/MSVC/CMake/CTest를 기본 구현 환경으로 둔다 **결정**: 기본 solver 구현과 validation은 C++17 이상, Visual Studio 17 2022 generator, x64 platform, Debug config, CMake, CTest로 수행한다. **이유**: FESA의 목표 환경은 Windows/MSVC 기반 C++이다. CMake/CTest는 solver source tree가 추가되거나 확장될 때 가장 일관된 build/test entry point다. -**트레이드오프**: Visual Studio solution-only workflow는 기본 지원하지 않는다. 필요하면 `HARNESS_VALIDATION_COMMANDS`로 override한다. +**트레이드오프**: FESA solver source는 CMake/CTest를 기본으로 유지한다. Harness 자체는 `.harness/config.json`에 solution과 test command를 명시한 직접 MSBuild 프로젝트도 검증할 수 있지만, 이는 FESA 제품이 solution-only workflow를 지원한다는 의미가 아니다. ### ADR-003: Abaqus `.inp` full compatibility가 아니라 기능별 keyword subset을 지원한다 **결정**: FESA parser는 Abaqus keyword/data/comment line 규칙을 따르되, 기능별로 승인된 keyword subset만 지원한다. 미지원 keyword는 명확한 diagnostic을 남긴다. @@ -75,9 +75,16 @@ FESA의 architecture decision은 solver correctness, verification traceability, **트레이드오프**: reference 준비가 느려질 수 있다. 대신 검증 기준의 신뢰도와 감사 가능성이 높아진다. -### ADR-011: C++ production 변경은 TDD guard와 workspace validation을 통과해야 한다 -**결정**: C++ production file 변경은 관련 C++ test file이 없으면 차단한다. 기본 검증은 `python -m unittest discover -s scripts -p "test_*.py"`와 `python scripts/validate_workspace.py`를 사용한다. +### ADR-011: 구형 단일 검증 진입점 계약을 폐기한다 -**이유**: FEM solver 결함은 작은 부호, DOF ordering, integration rule 오류에서도 발생한다. 테스트 없는 변경을 막아야 reference validation 이전 단계에서 회귀를 줄일 수 있다. +**상태**: ADR-012로 대체됨. -**트레이드오프**: 초기 scaffolding 작업에서 guard가 엄격하게 느껴질 수 있다. 문서, CMake 설정, Harness metadata는 guard 대상에서 제외한다. +**결정**: 삭제된 legacy validation entry point, legacy Python test discovery, 환경 변수 기반 command override를 Harness의 기본 검증 계약으로 사용하지 않는다. + +### ADR-012: Harness는 계획, 실행, Hook 검증의 세 계층으로 운영한다 + +**결정**: 계획은 `.agents/skills/harness`, Step 실행과 Git 상태 관리는 `scripts/execute.py`, 도구 호출 전 정책과 종료 전 검증은 `.codex/hooks.json`에 연결된 `scripts/hooks/`가 담당한다. C++ production 변경은 관련 테스트 파일이 있어야 하며, 실제 RED 실패와 GREEN 성공은 같은 Step 안에서 확인한다. Stop은 `.harness/config.json` 또는 자동 감지 결과로 전체 MSVC build/test를 검증한다. Harness Python 변경은 `uv run --with pytest python -m pytest -v -rs`로 검증한다. + +**이유**: 테스트 파일 존재 검사, TDD 실행 증거, 전체 build/test는 서로 다른 책임이다. 이를 분리하면 Hook이 보장하는 범위를 과장하지 않으면서 Step 종료 시 green 상태를 강제할 수 있다. + +**트레이드오프**: PreToolUse만으로 구현 전 RED 실행을 증명할 수 없으므로 Implementation report에 RED/GREEN 명령과 결과를 기록해야 한다. C/C++가 없는 저장소는 Stop이 통과하므로 Harness Python 검증은 별도 acceptance command로 유지한다. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 66f8e5e..2d32271 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -9,7 +9,7 @@ FESA의 아키텍처 목표는 Abaqus `.inp` subset을 내부 semantic model로 - sparse linear algebra backend isolation - deterministic verification - incremental feature addition -- Harness 기반 TDD와 workspace validation +- Harness 기반 TDD ## 디렉토리 구조 ```text @@ -31,6 +31,9 @@ src/ analysis/ # static, modal, dynamic, nonlinear procedure drivers results/ # recovery, field/history output, diagnostics validation/ # comparison metrics and tolerance helpers + math/ + Vector/ # managing double array including vector operation, vector operation must use BLAS from MKL + Matrix/ # managing double array including matrix operation, matrix operation must use BLAS from MKL tests/ unit/ integration/ @@ -43,24 +46,29 @@ reference/ _reactions.csv _internalforces.csv _stresses.csv +.agents/ + skills/ # Harness and review skills .codex/ - hooks/ # Codex hook scripts - skills/ # FESA solver and Harness instructions + hooks.json # PreToolUse/Stop hook registration + agents/ # FESA workflow custom agents + skills/ # FESA solver workflow skills docs/ # Product, architecture, ADR, workflow artifacts scripts/ execute.py # Phase step executor - validate_workspace.py # Default validation entry point - test_*.py # Harness self-tests + hooks/ # PreToolUse/Stop hook implementations + msvc_harness/ # MSVC project discovery and validation adapters phases/ # Optional generated phase plans ``` ## Harness Execution Layer -`scripts/execute.py`: -- creates or checks out `codex/` -- refuses to run on a dirty worktree -- requires per-step `allowed_paths` -- stages only explicit allowed paths and runner housekeeping files -- runs Python Harness self-tests and workspace validation before every runner-created commit + +Harness는 solver core와 분리된 세 계층의 개발 운영 인프라다. + +- 계획 계층: `.agents/skills/harness`가 사용자 승인 전 Step 초안을 만들고, 승인 후 `phases/` 파일을 생성한다. +- 실행 계층: `scripts/execute.py`가 `feat-` 브랜치에서 Step마다 독립 Codex 세션을 실행하고 상태와 커밋을 관리한다. +- 검증 계층: `.codex/hooks.json`이 `scripts/hooks/pre_tool_use.py`와 `scripts/hooks/stop_validation.py`를 연결한다. Stop 검증은 `scripts/msvc_harness/`를 통해 MSVC build와 test를 실행한다. + +Runner는 `git add -A`를 사용하므로 clean worktree 또는 별도 Git worktree가 실행 전제다. 전체 동작 계약은 `docs/HARNESS_WORKFLOW.md`, 설치와 `.harness/config.json` 설정은 `docs/HARNESS.md`를 source of truth로 삼는다. ## 모듈 경계 - `core`는 외부 라이브러리에 의존하지 않는다. @@ -148,6 +156,9 @@ Results ├── ResultFrame ├── FieldOutput └── HistoryOutput + +Vector +Matrix ``` ## 상태 관리 @@ -231,41 +242,3 @@ Schema requirements: - Abaqus reference results는 `reference//` 아래 CSV 파일이다. - Verification은 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준으로 FESA HDF5 rows와 Abaqus reference CSV rows를 비교한다. - FESA HDF5에서 추출한 deterministic CSV view는 optional debugging/review artifact이며 공식 solver output 또는 reference artifact가 아니다. - -## Test Architecture -- unit: parser, DOF map, shape functions, material law, sparse assembly, HDF5 schema -- integration: small `.inp` to HDF5 end-to-end -- reference: FESA `results.h5` rows and Abaqus reference CSV rows comparison -- physics: equilibrium, sign, symmetry, rigid body mode, stress sanity -- harness: hooks, phase executor, workspace validation - -## 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 -``` diff --git a/docs/HARNESS.md b/docs/HARNESS.md new file mode 100644 index 0000000..a7cf917 --- /dev/null +++ b/docs/HARNESS.md @@ -0,0 +1,124 @@ +# Harness 운영 가이드 + +## Requirements + +Windows, Python 3.10 이상, Codex CLI가 필요하다. CMake 프로젝트에는 Visual Studio의 +Desktop development with C++ 워크로드와 MSBuild, CMake/CTest를 설치한다. + +## 프로젝트 자동 감지 + +프로젝트 형식은 다음 순서로 결정한다: `.harness/config.json`의 명시적 type, 루트의 +CMake metadata, 하나의 `.sln`, 하나의 `.vcxproj` 순서다. C/C++가 아닌 저장소는 +건너뛰며, C/C++ 파일은 있지만 CMake/solution metadata가 없는 orphan-C++ 저장소는 +오류로 처리한다. + +설정 파일은 선택 사항이다. 기본 자동 감지와 `.harness/build` 경로를 그대로 사용할 +때는 만들지 않아도 된다. 프로젝트별 override가 필요하면 다음처럼 예시를 복사한다. + +```powershell +Copy-Item .harness/config.example.json .harness/config.json +``` + +계획을 승인해 phase 파일을 만든 뒤 Executor를 실행한다. + +```powershell +python scripts/execute.py +python scripts/execute.py --push +``` + +## Harness Python 검증 + +이 저장소의 테스트와 최종 acceptance 검증은 pytest를 시스템 Python에 설치하지 않고 +다음 명령으로 실행한다. + +```powershell +uv run --with pytest python -m pytest -v -rs +``` + +## CMake preset 설정 + +`projectType`을 `cmake`로 지정하거나 자동 감지를 사용한다. `cmake.sourceDir`, +`binaryDir`, `configurePreset`, `buildPreset`, `testPreset`은 preset을 사용할 때 함께 +지정해야 한다. 빌드 산출물은 저장소의 `.harness/build/`처럼 격리된 경로에 둔다. + +```json +{ + "version": 1, + "projectType": "cmake", + "cmake": { + "sourceDir": ".", + "binaryDir": "out/build/windows-debug", + "configurePreset": "windows-debug", + "buildPreset": "windows-debug", + "testPreset": "windows-debug" + } +} +``` + +```powershell +cmake --preset windows-debug +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +``` + +Preset을 쓰지 않는 경우에는 같은 격리된 build directory를 명시한다. + +```powershell +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure +``` + +## 직접 MSBuild 설정 + +`projectType`을 `msbuild`로 설정하면 `msbuild.solution`, `configuration`, `platform`을 +지정한다. 직접 MSBuild 프로젝트에서는 `msbuild.testCommand`가 필수이며, 테스트 실행 +파일과 인수를 JSON 배열로 적는다. + +```json +{ + "version": 1, + "projectType": "msbuild", + "msbuild": { + "solution": "MyProject.sln", + "configuration": "Debug", + "platform": "x64", + "testCommand": ["build/tests/Debug/MyProjectTests.exe"] + } +} +``` + +```powershell +MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64 +.\build\tests\Debug\MyProjectTests.exe +``` + +## TDD 확장 + +`tdd.testRoots`와 `tdd.testPatterns`로 테스트 위치와 이름을 확장한다. 패턴마다 +`{stem}`이 필요하다. `main`, 테스트, 외부 의존성, 생성 파일, build directory 같은 +기본 제외 항목은 Harness가 관리하며, `tdd.exclude`의 사용자 제외 항목은 이를 +대체하지 않고 추가한다. + +```json +{ + "version": 1, + "tdd": { + "testRoots": ["tests", "integration-tests"], + "testPatterns": ["{stem}_test.cpp", "test_{stem}.cpp"], + "exclude": ["legacy/generated/**"] + } +} +``` + +## 실패 복구 + +- Visual Studio C++ workload가 없으면 Installer에서 Desktop development with C++를 설치한 뒤 다시 실행한다. +- solution 또는 project가 여러 개라서 모호하면 `projectType`과 `msbuild.solution`을 명시한다. +- MSVC가 아닌 컴파일러가 감지되면 MSVC Developer Command Prompt에서 실행하거나 toolchain을 MSVC로 전환한다. +- CTest가 0개 테스트를 보고하면 `enable_testing()`과 테스트 등록을 확인한다. +- 직접 MSBuild 구성에 test command가 없으면 `msbuild.testCommand` 배열을 추가한다. +- timeout 또는 명령 실패 시 Stop 응답의 stage, 안전한 argv 배열, 작업 디렉터리, + 종료 코드와 출력 tail을 확인하고 해당 명령을 단독으로 다시 실행한다. Harness는 + 별도의 로그 파일을 만들지 않는다. diff --git a/docs/HARNESS_WORKFLOW.md b/docs/HARNESS_WORKFLOW.md new file mode 100644 index 0000000..134412a --- /dev/null +++ b/docs/HARNESS_WORKFLOW.md @@ -0,0 +1,607 @@ +# Harness Framework 동작 과정 + +이 문서는 자연어 요구사항을 받은 뒤 Harness Framework가 계획을 만들고, 독립된 +Codex 세션에서 Step을 실행하고, MSVC로 C++ 프로젝트를 검증하는 전체 과정을 +설명한다. 설치 및 설정 예시는 [Harness 운영 가이드](HARNESS.md)를 참고한다. + +## 1. 핵심 구조 + +Harness Framework는 다음 세 계층으로 구성된다. + +1. **계획 계층**: 요구사항을 분석하고 사용자가 승인할 실행 가능한 Step으로 변환한다. +2. **실행 계층**: Step Executor가 Step마다 독립 Codex 세션을 실행하고 상태와 Git + 커밋을 관리한다. +3. **검증 계층**: PreToolUse 훅이 편집 전 정책을 검사하고, Stop 훅이 종료 전 MSVC + 빌드와 테스트를 실행한다. + +전체 흐름은 다음과 같다. + +```text +사용자 요구사항 + ↓ +프로젝트 탐색 및 요구사항 논의 + ↓ +Step 초안 작성 + ↓ +사용자 승인 + ↓ +phases/index.json, task index, stepN.md 생성 + ↓ +Step Executor 시작 + ↓ +각 Step을 독립 Codex 세션에서 실행 + ├─ 도구 호출 전: PreToolUse 정책 검사 + └─ 응답 종료 전: Stop MSVC 빌드·테스트 + ↓ +성공: 커밋 후 다음 Step +실패: 수정 또는 최대 3회 재시도 +차단: 사용자 개입을 기다리며 중단 +``` + +자연어 요구사항만으로 Executor가 자동 시작되지는 않는다. 계획을 사용자가 승인하고 +phase 파일을 생성한 다음 `scripts/execute.py`를 실행해야 구현 루프가 시작된다. + +## 2. 요구사항 탐색과 구체화 + +예를 들어 사용자가 다음 요구사항을 전달했다고 가정한다. + +> CMake 기반 C++20 라이브러리에 `divide()` 함수를 추가하고, 0으로 나누면 예외를 +> 발생시키며 GoogleTest 테스트를 작성한다. + +계획을 작성하기 전에 다음 자료를 확인한다. + +- `AGENTS.md` +- `docs/PRD.md` +- `docs/ARCHITECTURE.md` +- `docs/ADR.md` +- 관련 제품 코드와 테스트 +- `.harness/config.json` + +이 탐색을 통해 다음 조건을 구체화한다. + +- 사용하는 MSVC toolset과 C++ 표준 +- CMake 프로젝트인지 Visual Studio solution/project인지 +- 테스트 프레임워크와 테스트 실행 방법 +- public header와 implementation의 의존성 방향 +- 수정할 모듈과 범위 밖 항목 +- 실행 가능한 Acceptance Criteria 명령 + +요구사항에 결정되지 않은 부분이 있으면 구현 전에 사용자와 논의한다. 위 예에서는 +예외 타입, 정수 또는 부동소수점 연산 여부, public API와 ABI 변경 허용 여부가 이에 +해당한다. + +## 3. 요구사항을 Step으로 분해 + +사용자가 구현 계획 작성을 요청하면 요구사항을 작은 Step으로 나눈다. Step 설계 +규칙은 [Harness Workflow](../.agents/skills/harness/SKILL.md)에 정의되어 있다. + +각 Step은 다음 조건을 만족해야 한다. + +- 하나의 모듈 또는 명확한 한 가지 책임만 다룬다. +- 다른 대화 내용을 참조하지 않아도 실행할 수 있도록 자기완결적으로 작성한다. +- 먼저 읽을 문서와 이전 Step의 관련 파일을 명시한다. +- 클래스와 함수 시그니처 수준으로 작업 범위를 설명한다. +- 실제 실행 가능한 빌드·테스트 명령을 Acceptance Criteria로 사용한다. +- 성공, 오류, 사용자 개입 필요 상태의 판정 기준을 적는다. +- 범위 밖 기능과 기존 테스트 회귀를 명시적으로 금지한다. + +예시 Step은 다음과 같은 내용을 포함할 수 있다. + +```text +Step 0: division-api + +읽어야 할 파일 +- AGENTS.md +- include/calculator.hpp +- src/calculator.cpp +- tests/calculator_test.cpp + +작업 +- divide(double lhs, double rhs)의 실패 테스트를 먼저 추가한다. +- rhs가 0이면 std::invalid_argument가 발생하도록 최소 구현한다. + +Acceptance Criteria +- CMake/MSBuild 빌드가 성공한다. +- 전체 테스트가 성공한다. +- 새로운 컴파일러 경고가 없다. +``` + +### TDD와 Step 경계 + +프로젝트 규칙은 실패하는 테스트를 먼저 요구하지만 Stop 훅은 Codex가 Step을 종료할 +때 전체 테스트 성공을 요구한다. 따라서 다음처럼 실패 상태를 Step 사이에 남겨둘 수 +없다. + +```text +Step 0: 실패하는 테스트만 추가하고 종료 +Step 1: 제품 코드를 구현해 테스트 통과 +``` + +실제 red-green 순서는 하나의 Codex 실행 안에서 완료되어야 한다. + +```text +테스트 작성 +→ 테스트 실패 확인 +→ 최소 제품 코드 구현 +→ 테스트 성공 확인 +→ Step 종료 +``` + +즉, 테스트가 구현보다 먼저 작성되는 순서는 지키되 각 Step은 최종적으로 green +상태여야 한다. + +## 4. 사용자 승인 후 생성되는 파일 + +Step 초안을 사용자가 승인한 뒤에만 다음 파일을 생성한다. + +```text +phases/ +├── index.json +└── add-division/ + ├── index.json + ├── step0.md + ├── step1.md + └── ... +``` + +### 4.1 Top-level index + +`phases/index.json`은 여러 task의 상태를 관리한다. + +```json +{ + "phases": [ + { + "dir": "add-division", + "status": "pending" + } + ] +} +``` + +### 4.2 Task index + +`phases/add-division/index.json`은 task 내부 Step의 상태를 관리한다. + +```json +{ + "project": "Calculator", + "phase": "add-division", + "steps": [ + { + "step": 0, + "name": "division-api", + "status": "pending" + } + ] +} +``` + +상태별 기록은 다음과 같이 나뉜다. + +| 상태 | Codex가 기록 | Executor가 기록 | +|---|---|---| +| `completed` | `summary` | `completed_at` | +| `error` | `error_message` | `failed_at` | +| `blocked` | `blocked_reason` | `blocked_at` | + +Executor는 task의 `created_at`과 Step의 `started_at`도 기록한다. `summary`는 다음 +독립 Codex 세션이 이전 Step의 핵심 산출물과 결정을 이해할 수 있도록 한 줄로 +작성한다. + +### 4.3 Step 파일 + +각 `stepN.md`에는 다음 내용이 들어간다. + +- 읽어야 할 파일 +- 작업 범위와 인터페이스 +- 핵심 동작 및 불변 조건 +- Acceptance Criteria 명령 +- 아키텍처·ADR·CRITICAL 규칙 확인 절차 +- 성공, 오류, 차단 상태 기록 방법 +- 범위 밖 변경 금지사항 + +Step은 독립 Codex 실행의 전체 작업 지시서이므로 이전 대화만 참조하는 표현을 넣지 +않는다. + +## 5. Step Executor 시작 + +계획 파일을 생성한 뒤 다음 명령으로 실행한다. + +```powershell +python scripts/execute.py add-division +``` + +완료된 브랜치를 원격 저장소에 자동 push하려면 `--push`를 추가한다. + +```powershell +python scripts/execute.py add-division --push +``` + +[Step Executor](../scripts/execute.py)는 시작할 때 다음 작업을 수행한다. + +1. phase 디렉터리와 task index가 존재하는지 검사한다. +2. 이전 실행에서 `error` 또는 `blocked`로 끝난 Step이 있는지 검사한다. +3. `feat-{phase-name}` 브랜치를 생성하거나 checkout한다. +4. `AGENTS.md`와 `docs/*.md`를 guardrail로 읽는다. +5. task의 `created_at`이 없으면 기록한다. +6. 첫 번째 `pending` Step부터 순차 실행한다. + +`AGENTS.md`와 모든 `docs/*.md` 내용은 각 Codex 프롬프트에 직접 삽입된다. 따라서 +이 문서들은 참고 자료가 아니라 실제 실행 입력이다. 서로 충돌하거나 placeholder가 +남아 있으면 Codex도 그 모순을 입력으로 받는다. + +## 6. Step마다 독립 Codex 세션 실행 + +Executor는 각 Step을 다음 형태의 독립 프로세스로 실행한다. + +```text +codex exec + --json + --sandbox workspace-write + --dangerously-bypass-hook-trust + --cd + - +``` + +Codex에 전달하는 프롬프트는 다음 내용의 조합이다. + +```text +AGENTS.md와 docs 문서 ++ 이전에 완료된 Step의 summary ++ 이전 시도의 오류(재시도인 경우) ++ Executor 공통 작업 규칙 ++ 현재 stepN.md +``` + +이전 Step의 전체 대화나 Codex 세션은 전달하지 않는다. task index에 기록한 +`summary`만 다음 Step에 누적한다. + +Codex 실행 결과의 exit code, stdout, stderr는 다음 파일에 저장한다. + +```text +phases/{task-name}/step{N}-output.json +``` + +## 7. 도구 호출 전 PreToolUse 검사 + +[`.codex/hooks.json`](../.codex/hooks.json)은 shell 및 파일 편집 도구에 +[PreToolUse 훅](../scripts/hooks/pre_tool_use.py)을 등록한다. Codex가 실제 명령이나 +편집을 수행하기 전에 이 훅이 요청을 검사한다. + +### 7.1 위험 명령 차단 + +다음 유형의 명령은 요구사항과 관계없이 차단한다. + +- `git reset --hard` +- `git push --force` 또는 `--force-with-lease` +- `rm -rf` +- `Remove-Item -Recurse -Force` +- `rmdir /s /q` +- `DROP TABLE` + +위험 패턴이 발견되면 훅은 차단 이유를 stderr로 출력하고 종료 코드 2를 반환한다. +그러면 해당 도구 호출은 실행되지 않는다. + +### 7.2 C++ TDD 검사 + +`apply_patch`, `Edit`, `MultiEdit`, `Write`로 다음 C/C++ 확장자의 파일을 편집하려 +하면 [TDD 정책](../scripts/msvc_harness/tdd_policy.py)을 검사한다. + +```text +.c .cc .cpp .cxx .h .hpp .hxx +``` + +일반 제품 코드를 수정하려면 대응되는 테스트 파일이 먼저 존재해야 한다. 예를 들어 +`src/calculator.cpp`의 기본 대응 테스트 이름은 다음과 같다. + +```text +calculator_test.cpp +calculator_tests.cpp +test_calculator.cpp +calculator.test.cpp +``` + +테스트는 다음 위치에서 검색한다. + +- `.harness/config.json`의 `tdd.testRoots` +- 제품 파일과 같은 디렉터리 아래 `tests/` +- 제품 파일과 같은 디렉터리 아래 `test/` + +다음 파일과 디렉터리는 대응 테스트 존재 검사가 면제된다. + +- 테스트 파일 자체 +- `main.cpp` +- `.harness/build/**`, `build/**`, `out/**` +- `cmake-build-*/**` +- `third_party/**`, `external/**`, `vendor/**` +- `generated/**` +- `tdd.exclude`에 추가한 경로 + +`tdd.exclude`는 기본 제외 항목을 대체하지 않고 추가한다. + +### 7.3 TDD 검사가 보장하는 범위 + +현재 TDD 훅이 직접 보장하는 것은 대응되는 이름의 테스트 파일이 존재한다는 +사실이다. 다음 항목까지 증명하지는 않는다. + +- 테스트가 이번 요구사항을 실제로 검증하는가 +- 구현 전에 테스트가 실제로 실패했는가 +- 테스트의 assertion과 경계 조건이 충분한가 +- 기존 테스트 파일을 이번 변경과 함께 수정했는가 + +또한 shell 명령의 리다이렉션 등으로 C++ 파일을 쓰는 경우 shell 위험 패턴 검사는 +적용되지만 경로 기반 TDD 검사는 적용되지 않는다. 따라서 이 훅은 완전한 TDD +증명기가 아니라 테스트 우선 편집을 유도하는 guardrail이다. + +## 8. Codex 종료 전 Stop 검증 + +Codex가 Step 작업을 끝내고 응답을 종료하려 하면 +[Stop 훅](../scripts/hooks/stop_validation.py)이 실행된다. Stop 훅은 변경 파일만이 +아니라 발견된 C/C++ 프로젝트 전체를 빌드하고 테스트한다. + +### 8.1 저장소 루트와 재진입 방지 + +Stop 훅은 `git rev-parse --show-toplevel`로 프로젝트 루트를 결정한다. Git 저장소를 +찾을 수 없으면 현재 디렉터리를 사용한다. + +빌드나 테스트의 자식 프로세스에는 `CODEX_STOP_VALIDATION_ACTIVE=1`을 전달한다. +같은 훅이 자식 프로세스에서 다시 진입하면 즉시 성공 처리하여 검증 재귀를 막는다. + +### 8.2 설정 로드 + +[설정 로더](../scripts/msvc_harness/config.py)는 `.harness/config.json`을 읽는다. +파일이 없으면 다음 기본값을 사용한다. + +- `version`: 1 +- `projectType`: `auto` +- CMake source: 저장소 루트 +- preset 미사용 시 binary directory: `.harness/build` +- configuration: `Debug` +- platform: `x64` +- 테스트 루트: `tests`, `test` +- 기본 테스트 이름 패턴 네 개 + +설정은 다음 조건을 엄격하게 검사한다. + +- 알 수 없는 필드를 거부한다. +- `version`은 숫자 1만 허용한다. +- `projectType`은 `auto`, `cmake`, `msbuild`만 허용한다. +- 저장소 상대 경로만 허용한다. +- 저장소 밖으로 해석되는 경로를 거부한다. +- CMake preset을 사용하면 `configurePreset`, `buildPreset`, `testPreset`, + `binaryDir`를 모두 요구한다. +- 모든 `tdd.testPatterns`에 `{stem}`을 요구한다. + +### 8.3 프로젝트 자동 감지 + +[프로젝트 탐색기](../scripts/msvc_harness/discovery.py)는 다음 순서로 프로젝트를 +선택한다. + +1. `projectType: cmake` 또는 `projectType: msbuild` 명시 설정 +2. 루트의 `CMakePresets.json` +3. 루트의 `CMakeUserPresets.json` +4. 루트의 `CMakeLists.txt` +5. 루트의 단일 `.sln` +6. 루트의 단일 `.vcxproj` + +자동 감지 결과는 다음처럼 처리한다. + +| 저장소 상태 | 결과 | +|---|---| +| CMake metadata가 있음 | CMake 프로젝트 선택 | +| 하나의 `.sln` 또는 `.vcxproj`가 있음 | MSBuild 프로젝트 선택 | +| 여러 solution/project가 있음 | 설정으로 하나를 지정하라는 오류 | +| C/C++ 파일과 build metadata가 모두 없음 | 검증할 프로젝트가 없으므로 통과 | +| C/C++ 파일은 있지만 build metadata가 없음 | orphan C++ 프로젝트 오류 | + +### 8.4 MSVC 도구 탐색 + +[도구 탐색기](../scripts/msvc_harness/toolchain.py)는 `vswhere.exe`로 다음을 +확인한다. + +- Visual Studio 설치 경로 +- Desktop development with C++ workload +- `MSBuild.exe` + +CMake 프로젝트에서는 다음 우선순위로 CMake와 CTest를 선택한다. + +1. PATH에서 발견한 독립 `cmake.exe`와 `ctest.exe` +2. Visual Studio에 번들된 CMake와 CTest + +따라서 새로 설치한 CMake의 `bin` 디렉터리가 PATH에 반영되어 있으면 독립 CMake를 +우선 사용한다. + +## 9. 빌드 시스템별 검증 계획 + +### 9.1 CMake preset 미사용 + +[CMake adapter](../scripts/msvc_harness/adapters/cmake.py)는 다음 검증 계획을 만든다. + +```powershell +cmake -S -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure +``` + +명령 성공 외에 다음 결과도 검사한다. + +- 생성된 CMake compiler metadata의 `CMAKE_CXX_COMPILER_ID`가 `MSVC`인가 +- CTest JSON에 한 개 이상의 테스트가 있는가 + +따라서 빌드가 성공해도 MinGW 등 다른 컴파일러를 사용했거나 CTest가 테스트를 한 +개도 발견하지 못하면 실패한다. + +### 9.2 CMake preset 사용 + +`.harness/config.json`에 preset을 완전히 지정하면 다음 형태로 실행한다. + +```powershell +cmake --preset +cmake --build --preset +ctest --preset --show-only=json-v1 +ctest --preset --output-on-failure +``` + +이 경우 모든 명령은 `cmake.sourceDir`에서 실행하고 compiler metadata 검사는 설정한 +`binaryDir`에서 수행한다. + +### 9.3 직접 MSBuild + +[MSBuild adapter](../scripts/msvc_harness/adapters/msbuild.py)는 다음 순서로 실행한다. + +```powershell +MSBuild.exe /m /nologo ` + /p:Configuration= ` + /p:Platform= + + +``` + +직접 MSBuild 프로젝트는 표준 테스트 탐색 명령이 없으므로 +`.harness/config.json`의 `msbuild.testCommand`가 반드시 필요하다. 이 값이 없으면 +Stop 검증이 실패한다. + +## 10. 명령 실행 안전성과 제한시간 + +[검증 실행기](../scripts/msvc_harness/process.py)는 다음 안전 규칙을 적용한다. + +- 명령을 shell 문자열이 아닌 argv 배열로 실행한다. +- `shell=False`를 사용한다. +- 각 명령의 working directory가 저장소 내부인지 검사한다. +- 명령별 제한시간과 Stop 전체 제한시간 중 더 짧은 값을 적용한다. +- 종료 코드가 0이 아니면 즉시 해당 stage를 실패 처리한다. + +Stop 훅의 전체 제한시간은 저장소 탐색, toolchain 탐색, configure, build, test discovery, +test를 모두 포함해 1,800초다. `.codex/hooks.json`의 Stop command timeout도 1,800초다. + +실패 메시지에는 다음 진단 정보를 포함한다. + +- 실패 stage +- 안전하게 표현한 argv 배열 +- working directory +- 종료 코드 +- stdout과 stderr의 마지막 8,000자 + +Harness는 별도 빌드 로그 파일을 생성하지 않는다. + +## 11. 성공, 실패, 차단 처리 + +### 11.1 Stop 검증 성공 + +빌드와 테스트가 모두 성공하면 Stop 훅은 출력 없이 종료한다. Codex가 정상 종료하면 +Executor가 task index를 다시 읽는다. + +Codex가 Step을 다음처럼 기록한 경우: + +```json +{ + "step": 0, + "name": "division-api", + "status": "completed", + "summary": "divide API와 0 나누기 테스트를 추가함" +} +``` + +Executor는 `completed_at`을 기록하고 변경사항을 커밋한 뒤 다음 `pending` Step을 +실행한다. + +### 11.2 Stop 검증 실패 + +Stop 훅은 Codex hook protocol에 따라 다음 형태의 응답을 출력한다. + +```json +{ + "continue": false, + "stopReason": "build failed ...", + "systemMessage": "build failed ..." +} +``` + +Codex 프로세스에 대한 훅 자체의 종료 코드는 0이지만 `continue: false`가 Codex의 +응답 종료를 막는다. Codex는 같은 세션에서 오류를 확인하고 수정을 계속한다. + +### 11.3 Executor 재시도 + +Codex 프로세스가 끝났는데 Step 상태가 `completed` 또는 `blocked`가 아니면 Executor가 +새 Codex 세션으로 재시도한다. + +```text +첫 번째 시도 실패 +→ 오류를 다음 프롬프트에 삽입 +→ 두 번째 독립 Codex 실행 +→ 다시 실패하면 세 번째 독립 Codex 실행 +→ 세 번째도 실패하면 error 기록 후 종료 +``` + +즉, 실패 복구에는 두 층이 있다. + +1. Stop 훅이 같은 Codex 세션에서 수정하도록 요구한다. +2. 세션 자체가 성공하지 못하면 Executor가 새 세션으로 최대 3회 재시도한다. + +### 11.4 사용자 개입 필요 + +인증, API 키, 수동 설치처럼 Codex가 자동으로 해결할 수 없는 문제가 있으면 Step을 +`blocked`로 기록한다. Executor는 `blocked_at`과 top-level 상태를 갱신하고 종료 코드 +2로 중단한다. + +재개하려면 원인을 해결하고 해당 Step을 `pending`으로 되돌린 뒤 +`blocked_reason`을 제거하고 다시 실행한다. `error`도 같은 방식으로 `pending`으로 +되돌리고 `error_message`를 제거한 뒤 재실행한다. + +## 12. Git 커밋과 phase 완료 + +Step이 성공하면 제품 변경과 Harness metadata를 분리해 다음 형식으로 커밋한다. + +```text +feat(add-division): step 0 — division-api +chore(add-division): step 0 output +``` + +두 번째 커밋의 `output`은 task index의 Step 상태와 summary 같은 Harness metadata를 +뜻한다. 원시 Codex 실행 기록인 `stepN-output.json`은 `.gitignore` 대상이며 커밋에 +포함되지 않는다. + +모든 Step이 완료되면 Executor는 다음 작업을 수행한다. + +- task의 `completed_at` 기록 +- `phases/index.json`의 task 상태를 `completed`로 변경 +- 최종 metadata 커밋 +- `--push` 사용 시 `origin/feat-{phase-name}`으로 push + +커밋 과정은 `git add -A`를 사용한다. 실행 전에 작업 트리에 관련 없는 사용자 +변경사항이 남아 있으면 그 변경도 Step 커밋에 포함될 수 있다. 따라서 깨끗한 +worktree 또는 별도 Git worktree에서 실행하는 것이 안전하다. + +## 13. 요구사항 종류별 동작 + +| 받은 요구사항 또는 변경 | PreToolUse 동작 | Stop 동작 | +|---|---|---| +| 새 C++ 제품 파일 추가 | 대응 테스트가 먼저 없으면 차단 | 전체 빌드·테스트 | +| 기존 C++ 구현 또는 header 수정 | 대응 테스트 파일 존재 여부 검사 | 전체 빌드·테스트 | +| 테스트 파일 추가 | TDD 차단 없이 허용 | 모든 테스트가 성공해야 종료 | +| `main.cpp` 수정 | TDD 대응 테스트 검사 면제 | 전체 빌드·테스트 | +| 문서, JSON, Python 수정 | C++ TDD 검사 없음 | C++ 프로젝트가 있으면 전체 검증 | +| 위험한 Git 또는 삭제 명령 | 즉시 차단 | 도달하지 않음 | +| C++ 파일은 있지만 build metadata 없음 | 편집은 허용될 수 있음 | orphan 프로젝트 오류 | +| 직접 MSBuild인데 `testCommand` 없음 | 편집은 허용될 수 있음 | 설정 오류로 종료 차단 | +| C/C++가 전혀 없는 저장소 | 관련 편집 검사 없음 | 검증할 프로젝트가 없어 통과 | + +## 14. 적용 전 준비사항 + +이 저장소는 대상 C++ 프로젝트에 맞게 채워 사용하는 템플릿이다. 실행 전 다음을 +확인한다. + +1. `AGENTS.md`의 프로젝트명, toolset, C++ 표준, 테스트 프레임워크, CRITICAL 규칙을 + 실제 값으로 교체한다. +2. `docs/PRD.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`의 placeholder와 예시를 실제 + 프로젝트 정보로 교체한다. +3. 기본 자동 감지로 충분하지 않을 때만 `.harness/config.example.json`을 참고해 + `.harness/config.json`을 만든다. +4. CMake 또는 MSBuild metadata와 테스트 실행 방법을 확인한다. +5. `phases/`가 없다면 요구사항 논의와 계획 승인을 거쳐 task 파일을 먼저 만든다. +6. Executor 실행 전에 Git working tree가 깨끗한지 확인한다. + +특히 `AGENTS.md`와 `docs/*.md`는 각 Codex 실행에 그대로 주입된다. C++ 프로젝트에서 +TypeScript 예시나 미완성 placeholder가 남아 있으면 실제 작업 지시와 충돌할 수 있다. diff --git a/docs/PRD.md b/docs/PRD.md index f21d7db..af57755 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -20,11 +20,11 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기 6. `LinearSolver` adapter를 통한 MKL PARDISO backend와 향후 iterative solver 확장 7. HDF5 기반 `ResultStep` -> `ResultFrame` -> `FieldOutput`/`HistoryOutput` 저장 8. FESA HDF5 rows와 `reference//` 아래 Abaqus reference CSV rows의 직접 비교 -9. CMake/MSVC/x64/Debug, CTest, Harness validation, TDD guard 기반 개발 검증 +9. CMake/MSVC/x64/Debug, CTest 기반 개발 검증 ## V0 범위 -- 선형 정적 해석 골격 -- 첫 end-to-end 기능 후보: 1D truss/bar element +- 선형 정적 해석 파이프라인 구현 +- 첫 end-to-end 기능 후보: Isoparametric 3D Euler beam element - 최소 Abaqus keyword subset: - `*HEADING` - `*NODE` @@ -33,24 +33,17 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기 - `*ELSET` - `*MATERIAL` - `*ELASTIC` - - section keyword + - `*Beam General Section` - `*BOUNDARY` - `*CLOAD` - `*STEP` - `*STATIC` - output request subset -- displacement 중심의 최소 `AnalysisState` +- MKL PARDISO 기반 sparse direct solver +- displacements, reactions, elemental forces 중심의 verification - HDF5 result schema v0 - FESA HDF5 to Abaqus reference CSV comparison 계약 -## V1 범위 -- 2D plane stress/plane strain element -- 3D solid element -- MKL PARDISO 기반 sparse direct solve -- TBB element-local computation 병렬화 -- reference model portfolio 확장 -- nonlinear static, dynamic, frequency, heat transfer 해석을 위한 interface 확장점 - ## 기능 요구조건 | ID | 요구조건 | Acceptance Criteria | Verification Method | | --- | --- | --- | --- | @@ -61,7 +54,7 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기 | FESA-PRD-005 | FESA는 해석 중 변하는 물리량을 `AnalysisState`에 저장해야 한다. | displacement, force, residual, increment/iteration 상태가 step/frame 출력과 연결된다. | state unit test, integration test | | FESA-PRD-006 | FESA는 solver 결과를 HDF5 authoritative output `results.h5`로 저장해야 한다. | step/frame, field/history, metadata, diagnostics가 schema version과 함께 저장된다. | HDF5 schema test | | FESA-PRD-007 | FESA는 Abaqus reference CSV rows와 비교 가능한 deterministic row mapping을 제공해야 한다. | displacement, reaction, internal force, stress 등 검증 물리량의 row identity와 tolerance source가 명확하다. | reference comparison report | -| FESA-PRD-008 | FESA의 production C++ 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현해야 한다. | 관련 C++ test file이 있고 Harness TDD guard를 통과한다. | hook test, CTest | +| FESA-PRD-008 | FESA의 production C++ 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현해야 한다. | 관련 C++ test file, RED 실패와 후속 GREEN 성공 증거가 있고 Stop의 전체 MSVC build/test가 통과한다. | implementation report, Hook guardrail, CTest | | FESA-PRD-009 | FESA는 외부 라이브러리 API를 solver core에 직접 노출하지 않아야 한다. | MKL, TBB, HDF5 의존은 adapter module에 제한된다. | architecture review, dependency review | | FESA-PRD-010 | FESA 기능 완료는 reference comparison과 physics sanity 통과를 요구해야 한다. | 수치 tolerance와 물리 검토가 모두 pass이고 known limitation이 기록된다. | verification report, physics evaluation report | @@ -79,7 +72,7 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기 2. Research evidence complete: 정식화와 benchmark 근거가 신뢰도와 한계와 함께 정리되어 있다. 3. Formulation reviewed: 약형, shape function, B matrix, constitutive contract, 수치적분, output recovery가 검토되어 있다. 4. I/O contract approved: Abaqus keyword subset, internal model mapping, HDF5 result contract, reference CSV comparison row contract가 승인되어 있다. -5. Tests fail before implementation: 구현 전 실패해야 하는 C++/integration/reference test가 준비되어 있다. +5. Tests fail before implementation: C++/integration/reference test를 제품 코드보다 먼저 작성하고 같은 Step 안에서 RED 실패와 후속 GREEN 성공을 확인한다. 6. CMake/CTest pass: MSVC/x64/Debug 기준 configure, build, test가 통과한다. 7. Reference comparison pass: FESA `results.h5` rows와 Abaqus reference CSV rows가 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준 안에 있다. 8. Physics sanity pass: equilibrium, reaction consistency, displacement direction, symmetry, stress sanity가 검토되어 있다. @@ -92,4 +85,3 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기 - GUI 또는 postprocessor - Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow - Explicit dynamics, contact, plasticity, shell end-to-end 구현 -- JavaScript/TypeScript fallback 유지 diff --git a/docs/SOLVER_AGENT_DESIGN.md b/docs/SOLVER_AGENT_DESIGN.md index a284647..64a4043 100644 --- a/docs/SOLVER_AGENT_DESIGN.md +++ b/docs/SOLVER_AGENT_DESIGN.md @@ -12,6 +12,7 @@ - Abaqus나 Nastran을 Agent가 직접 실행하지 않는다. `reference//`에 저장된 `model.inp`, `metadata.json`, Abaqus reference CSV files를 검증 기준으로 사용한다. - 기본 개발 환경은 C++17 이상, MSVC, CMake, CTest이다. - 모든 기능은 tolerance 기준을 명시하고, 기준을 만족할 때만 배포 후보가 된다. +- Harness 운영은 `docs/HARNESS_WORKFLOW.md`의 계획, 독립 Step 실행, PreToolUse/Stop 검증 계층을 따른다. ## 전체 Agent 구성 @@ -168,15 +169,20 @@ C++ 코드를 구현하는 Agent이다. 빌드와 테스트를 실행하는 Agent이다. 책임: -- Harness validation을 실행한다. +- `.harness/config.json` 또는 자동 감지 결과에 맞는 MSVC build/test 명령을 실행한다. - MSVC x64 Debug CMake configure/build/CTest 결과를 수집한다. - 실패 로그를 요약하고 Correction Agent에 전달한다. -기본 검증 명령: +기본 CMake 검증 명령: ```powershell -python scripts/validate_workspace.py +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure ``` +Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`에 선언된 명령을 따른다. + 검증 대상: - CMake configure - MSVC Debug build @@ -301,7 +307,8 @@ flowchart TD 통과 조건: - CMake/MSVC/CTest validation이 통과한다. - 단위 테스트와 통합 테스트가 통과한다. -- Harness TDD guard를 만족한다. +- 관련 C++ test file이 있고 같은 구현 Step 안에 RED 실패와 후속 GREEN 성공 증거가 있다. +- Stop의 전체 MSVC build/test 검증이 통과한다. ### Gate 5: 레퍼런스 검증 통과 조건: @@ -352,11 +359,12 @@ Coordinator Agent는 분류 결과에 따라 Requirement, Formulation, I/O Defin ## 초기 적용 우선순위 1. 선형 정적 해석의 최소 골격 -2. 1D truss 또는 bar element -3. 2D plane stress/plane strain element -4. 3D solid element -5. material model 확장 -6. nonlinear 또는 dynamic analysis 확장 +2. Isoparametric 3D Euler beam element +3. 1D truss 또는 bar element +4. 2D plane stress/plane strain element +5. 3D solid element +6. material model 확장 +7. nonlinear 또는 dynamic analysis 확장 각 단계는 요구조건, 정식화, 테스트모델, 구현, 레퍼런스 비교, 배포 Gate를 독립적으로 통과해야 한다. diff --git a/docs/SOLVER_SKILL_DESIGN.md b/docs/SOLVER_SKILL_DESIGN.md index a43e01b..846f880 100644 --- a/docs/SOLVER_SKILL_DESIGN.md +++ b/docs/SOLVER_SKILL_DESIGN.md @@ -15,7 +15,8 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로 - Abaqus, Nastran 또는 reference solver 실행은 skill 범위에 포함하지 않는다. - Abaqus reference CSV 파일 생성/수정은 skill 범위에 포함하지 않는다. - C++ 구현 관련 skill은 C++17 이상, MSVC, CMake, CTest, TDD 원칙을 따른다. -- 기본 workspace validation 명령은 `python scripts/validate_workspace.py`이다. +- C++ 검증 명령은 `.harness/config.json` 또는 `docs/HARNESS.md`의 자동 감지 기본값을 따른다. +- Harness Python 변경은 `uv run --with pytest python -m pytest -v -rs`로 검증한다. ## Skill 구성 @@ -34,10 +35,10 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로 ## 개발 과정별 사용 예 -예시 기능: `linear-truss-1d` +예시 기능: `isoparametric-3d-euler-beam` 1. Requirement Agent는 `fesa-requirements-baseline`을 사용해 기능 범위, 제외 범위, 입력, 출력, 검증 물리량, tolerance, `Requirement Verification Matrix`를 작성한다. -2. Research Agent는 `fesa-research-evidence`를 사용해 truss/bar element 이론, benchmark 후보, source reliability, applicability limits를 정리한다. +2. Research Agent는 `fesa-research-evidence`를 사용해 3D Euler beam element 이론, benchmark 후보, source reliability, applicability limits를 정리한다. 3. Formulation Agent는 `fesa-formulation-spec`을 사용해 strong form, weak form, shape functions, B matrix, element stiffness, output recovery를 정리한다. 4. Numerical Review Agent는 `fesa-numerical-review`를 사용해 rigid body modes, patch test, stiffness symmetry, Jacobian, locking 위험을 검토하고 `pass-for-implementation-planning` 여부를 판단한다. 5. I/O Definition Agent는 `fesa-io-contract`를 사용해 지원할 Abaqus `.inp` keyword subset, `results.h5` schema, reference CSV comparison row schema를 정의한다. @@ -98,9 +99,17 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로 - 기본 검증 명령: ```powershell -python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_workspace.py -ctest -C Debug -R +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug -R --output-on-failure +ctest --test-dir .harness/build -C Debug --output-on-failure +``` + +Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`의 설정을 사용한다. Harness +Python, Hook, agent config를 변경한 경우에는 다음 명령도 실행한다. + +```powershell +uv run --with pytest python -m pytest -v -rs ``` - 실패는 `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`로 분류한다. @@ -148,7 +157,8 @@ ctest -C Debug -R ## 검증 기준 -Skill 구성 검증은 `scripts/test_fesa_solver_skills.py`가 담당한다. +Skill 구성은 실제 `.codex/skills/` 파일을 source of truth로 삼아 정적 계약과 repository +pytest suite로 검증한다. 검증 항목: @@ -163,15 +173,11 @@ Skill 구성 검증은 `scripts/test_fesa_solver_skills.py`가 담당한다. 검증 명령: ```powershell -python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_workspace.py +uv run --with pytest python -m pytest -v -rs ``` -Skill 구조 검증: - -```powershell -python C:\Users\user\.codex\skills\.system\skill-creator\scripts\quick_validate.py .codex\skills\ -``` +개별 skill schema를 점검할 때는 현재 Codex 설치에 포함된 `skill-creator` validator를 +사용하되 사용자 홈을 하드코딩한 경로를 프로젝트 계약으로 두지 않는다. ## v1 범위 diff --git a/docs/ProjectInitialPlanNote.md b/docs/archive/ProjectInitialPlanNote.md similarity index 98% rename from docs/ProjectInitialPlanNote.md rename to docs/archive/ProjectInitialPlanNote.md index 773f099..9b7fcaf 100644 --- a/docs/ProjectInitialPlanNote.md +++ b/docs/archive/ProjectInitialPlanNote.md @@ -1,5 +1,10 @@ # FESA 초기 문서 완성 계획 노트 +> **Historical / Superseded:** 이 문서는 2026-06-10 시점의 초기 조사와 실행 기록이다. +> 현재 제품 범위는 `docs/PRD.md`, Harness 운영 계약은 `docs/HARNESS_WORKFLOW.md`와 +> `docs/HARNESS.md`를 따른다. 아래의 구형 스킬명, 검증 명령, 기능 우선순위는 현재 +> 지침으로 사용하지 않는다. + ## 메타데이터 - 작성일: 2026-06-10 - 목적: `AGENTS.md`, `docs/PRD.md`, `docs/ARCHITECTURE.md`를 유한요소법 기반 구조해석 솔버 개발 프로젝트 문서로 완성하기 위한 조사 내용과 실행 계획 정리 diff --git a/docs/build-test-reports/README.md b/docs/build-test-reports/README.md index 63fb116..cc5b1f8 100644 --- a/docs/build-test-reports/README.md +++ b/docs/build-test-reports/README.md @@ -2,16 +2,17 @@ 이 디렉터리는 Build/Test Executor Agent가 작성하거나 제안하는 기능별 build/test 실행 리포트를 보관하는 위치다. -Build/Test Executor Agent는 Implementation Agent 이후 독립적으로 C++/MSVC/CMake/CTest 검증을 실행하고, 실패를 분류해 다음 agent로 handoff한다. 이 agent는 source code, tests, CMake files, requirements, formulations, I/O contracts, reference artifacts, tolerance policies를 수정하지 않는다. build artifacts와 test outputs는 `build/` 아래 생성될 수 있다. +Build/Test Executor Agent는 Implementation Agent 이후 독립적으로 C++/MSVC/CMake/CTest 검증을 실행하고, 실패를 분류해 다음 agent로 handoff한다. 이 agent는 source code, tests, CMake files, requirements, formulations, I/O contracts, reference artifacts, tolerance policies를 수정하지 않는다. 기본 build artifact는 `.harness/build/` 아래 생성된다. 기본 문서명은 `docs/build-test-reports/-build-test.md` 형식을 사용한다. ## Build/Test Executor Agent 역할 수행한다: -- `python scripts/validate_workspace.py`를 기본 검증 명령으로 실행한다. -- implementation plan/report에 명시된 경우 harness self-test와 feature-specific CTest를 실행한다. -- `HARNESS_VALIDATION_COMMANDS`, `CMakePresets.json`의 `msvc-debug`, 기본 CMake/MSVC x64 Debug 경로 중 어떤 검증 경로가 사용되었는지 기록한다. +- `.harness/config.json`과 프로젝트 자동 감지 결과를 확인하고 같은 build/test 경로를 독립 실행한다. +- implementation plan/report에 명시된 feature-specific CTest를 전체 검증 전에 실행한다. +- Harness Python, Hook, agent config 변경이 포함되면 `uv run --with pytest python -m pytest -v -rs`를 실행한다. +- CMake preset, 직접 MSBuild, 기본 CMake/MSVC x64 Debug 중 어떤 검증 경로가 사용되었는지 기록한다. - configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다. - command, exit code, duration, stdout/stderr tail, failed test name을 요약한다. - 실패 원인에 따라 Implementation Agent, Correction Agent, Reference Verification Agent, Implementation Planning Agent 중 handoff 대상을 제안한다. @@ -28,29 +29,43 @@ Build/Test Executor Agent는 Implementation Agent 이후 독립적으로 C++/MSV ## 실행 순서 -기본 순서는 implementation plan/report에 따라 다음 중 필요한 항목만 실행한다. +기본 순서는 implementation plan/report에 따라 다음 중 필요한 항목을 실행한다. ```powershell -python -m unittest discover -s scripts -p "test_*.py" -ctest -C Debug -R -python scripts/validate_workspace.py +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug -R --output-on-failure +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure ``` -`scripts/validate_workspace.py`의 command discovery 우선순위는 다음과 같다. +Harness Python, Hook, agent config 변경이 검증 범위에 포함되면 다음 명령을 먼저 실행한다. -1. `HARNESS_VALIDATION_COMMANDS` -2. `CMakePresets.json`의 `msvc-debug` -3. 기본 CMake/MSVC x64 Debug 명령 -4. `CMakeLists.txt`가 없고 override도 없으면 안내 메시지와 함께 성공 종료 +```powershell +uv run --with pytest python -m pytest -v -rs +``` + +프로젝트 선택 우선순위는 다음과 같다. + +1. `.harness/config.json`의 명시적 `projectType` +2. 루트의 CMake metadata +3. 루트의 단일 `.sln` +4. 루트의 단일 `.vcxproj` 기본 CMake/MSVC x64 Debug 명령은 다음과 같다. ```powershell -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 +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure ``` +CMake preset을 사용하면 configure/build/test preset과 `binaryDir`를 모두 +`.harness/config.json`에 지정한다. 직접 MSBuild는 solution/project와 +`msbuild.testCommand`를 지정한다. C/C++와 build metadata가 모두 없으면 검증 대상이 +없으므로 통과하지만, C/C++ 파일만 있고 build metadata가 없으면 오류다. + ## 문서 템플릿 ```markdown @@ -69,17 +84,19 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug - generator: Visual Studio 17 2022 | - platform: x64 | - config: Debug | -- build_dir: build/msvc-debug | -- active_override_env_vars: HARNESS_VALIDATION_COMMANDS | HARNESS_CMAKE_GENERATOR | HARNESS_CMAKE_PLATFORM | HARNESS_CMAKE_CONFIG | HARNESS_BUILD_DIR | none -- command_discovery_path: HARNESS_VALIDATION_COMMANDS | CMakePresets.json msvc-debug | default CMake/MSVC x64 Debug | no-CMake informational success +- build_dir: .harness/build | +- harness_config: .harness/config.json | absent-defaults +- project_selection: configured cmake | configured msbuild | auto CMake | auto MSBuild | no C/C++ project +- command_discovery_path: CMake preset | direct MSBuild | default CMake/MSVC x64 Debug | no C/C++ project ## Command Log Summary | order | command | exit_code | duration | stdout_stderr_tail | | --- | --- | --- | --- | --- | -| 1 | python -m unittest discover -s scripts -p "test_*.py" | | | | -| 2 | ctest -C Debug -R | | | | -| 3 | python scripts/validate_workspace.py | | | | +| 1 | uv run --with pytest python -m pytest -v -rs (when applicable) | | | | +| 2 | | | | | +| 3 | | | | | +| 4 | | | | | ## Validation Results @@ -137,7 +154,7 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug - 모든 실행 명령과 exit code를 기록해야 한다. - 실패 로그는 전체 원문을 복제하지 않고 마지막 핵심 구간과 실패 원인을 요약한다. - configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다. -- no-CMake 상황은 `scripts/validate_workspace.py` 정책대로 안내 메시지와 성공 종료로 기록한다. +- C/C++와 build metadata가 모두 없는 상황만 `no C/C++ project` 성공으로 기록한다. C/C++ 파일이 있는데 build metadata가 없으면 `environment` 또는 `configure` 실패로 기록한다. - 성공 판정은 build/test 통과까지만 의미한다. - reference tolerance, physics validation, release readiness는 판정하지 않는다. - upstream 계약 문제는 Implementation Agent에 임의 수정으로 넘기지 않고 적절한 upstream agent로 handoff한다. diff --git a/docs/coordination/README.md b/docs/coordination/README.md index ed066b1..278dd92 100644 --- a/docs/coordination/README.md +++ b/docs/coordination/README.md @@ -53,7 +53,7 @@ INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT - Reference Verification Agent report - Physics Evaluation Agent report - Release Agent report -- validation command evidence: `python scripts/validate_workspace.py` +- Build/Test report의 `.harness/config.json` 또는 자동 감지 기반 MSVC build/test command evidence ## 문서 템플릿 @@ -184,6 +184,6 @@ INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT ## 검증 기준 -- Coordinator Agent config와 문서 템플릿 검증은 Python unittest로 수행한다. -- workspace 검증은 `python scripts/validate_workspace.py`를 사용한다. -- 현재 repository에 CMake 프로젝트가 없으면 harness 정책에 따라 no-CMake validation 경로가 성공으로 기록될 수 있다. +- Coordinator Agent config와 문서 템플릿에 자동화된 Python 검증이 있으면 `uv run --with pytest python -m pytest -v -rs`로 실행한다. +- C++ build/test evidence는 `.harness/config.json` 또는 Harness 자동 감지 기본값에 따른 Build/Test report에서 확인한다. +- C/C++ 파일과 build metadata가 모두 없을 때만 `no C/C++ project` 성공을 허용한다. C/C++ 파일만 있고 build metadata가 없으면 통과 evidence로 사용하지 않는다. diff --git a/docs/corrections/README.md b/docs/corrections/README.md index 6992d3a..cea980e 100644 --- a/docs/corrections/README.md +++ b/docs/corrections/README.md @@ -12,8 +12,8 @@ Correction Agent는 Build/Test Executor Agent, Reference Verification Agent, Phy - 실패 로그와 implementation report를 읽고 failure classification을 먼저 확정한다. - configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다. - implementation-owned failure에 한해 source/header/test/CMake를 최소 수정한다. -- 수정 후 targeted command를 먼저 실행하고 `python scripts/validate_workspace.py`를 실행한다. -- harness, hook, agent config 관련 수정에서는 `python -m unittest discover -s scripts -p "test_*.py"`도 실행한다. +- 수정 후 targeted command를 먼저 실행하고 `.harness/config.json` 또는 자동 감지 기본값에 따른 전체 MSVC build/test를 실행한다. +- Harness Python, Hook, agent config 관련 수정에서는 `uv run --with pytest python -m pytest -v -rs`도 실행한다. - 반복 실패 또는 upstream 계약 문제를 Coordinator Agent나 관련 upstream agent로 handoff한다. 수행하지 않는다: @@ -40,11 +40,16 @@ TRIAGE -> MINIMAL FIX -> VERIFY -> REPORT ```powershell -python scripts/validate_workspace.py -python -m unittest discover -s scripts -p "test_*.py" +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --show-only=json-v1 +ctest --test-dir .harness/build -C Debug --output-on-failure ``` -`python -m unittest discover -s scripts -p "test_*.py"`는 harness, hook, agent config, Python validation behavior가 correction 범위에 포함될 때 실행한다. +Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`의 설정을 따른다. Harness +Python, Hook, agent config, Python validation behavior가 correction 범위에 포함될 때는 +`uv run --with pytest python -m pytest -v -rs`도 실행한다. Stop 검증은 응답 종료 전에 +같은 전체 프로젝트 검증을 다시 수행한다. ## Failure Classification @@ -53,7 +58,7 @@ python -m unittest discover -s scripts -p "test_*.py" - `link`: linker, symbol resolution, target dependency 실패 - `test`: CTest, unit, integration, parser/I/O, ordinary regression test 실패 - `reference-comparison`: 저장된 reference artifact와 deterministic comparison 실패 -- `harness`: Python harness self-test, TDD guard, hook, validation script 실패 +- `harness`: Python Harness test, PreToolUse/Stop Hook, config loading, discovery, adapter validation 실패 - `environment`: MSVC, CMake, Python, path, permission, generator, local dependency 문제 - `upstream-contract`: requirements, formulation, I/O, reference artifact, tolerance, implementation plan 불일치 또는 누락 @@ -104,8 +109,8 @@ Excluded files: | order | command | exit_code | result | evidence | | --- | --- | --- | --- | --- | | 1 | | | pass | fail | | -| 2 | python scripts/validate_workspace.py | | pass | fail | | -| 3 | python -m unittest discover -s scripts -p "test_*.py" | | pass | fail | skipped | | +| 2 | | | pass | fail | | +| 3 | uv run --with pytest python -m pytest -v -rs | | pass | fail | skipped | | ## Traceability diff --git a/docs/implementation-plans/README.md b/docs/implementation-plans/README.md index 5cb8d83..86efb4a 100644 --- a/docs/implementation-plans/README.md +++ b/docs/implementation-plans/README.md @@ -15,7 +15,7 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정 - CMake/CTest target, `add_test`, label, `ctest -C Debug` 검증 계획을 정의한다. - candidate source/header/test/CMake 파일과 ownership boundary를 제안한다. - requirement, task, test, reference model, acceptance criterion을 Acceptance Traceability Matrix로 연결한다. -- `python scripts/validate_workspace.py`를 포함한 validation command를 명시한다. +- `.harness/config.json` 또는 자동 감지 기본값에서 해석되는 MSVC build/test 명령과 feature-specific command를 명시한다. 수행하지 않는다: - C++ 코드를 구현하지 않는다. @@ -80,8 +80,8 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정 - add_test_needs: - labels: unit | integration | reference | parser | io - msvc_config: Debug -- expected_feature_command: ctest -C Debug -R -- workspace_validation: python scripts/validate_workspace.py +- expected_feature_command: ctest --test-dir .harness/build -C Debug -R --output-on-failure +- full_validation_source: .harness/config.json | Harness auto detection ## Candidate Files and Ownership @@ -107,11 +107,17 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정 ## Validation Commands ```powershell -python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_workspace.py -ctest -C Debug -R +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug -R --output-on-failure +ctest --test-dir .harness/build -C Debug --output-on-failure ``` +Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`에 해석 가능한 명령을 적는다. +Harness Python, Hook, agent config 변경이 계획 범위에 포함되면 +`uv run --with pytest python -m pytest -v -rs`도 추가한다. Stop 검증은 Step 종료 전 전체 +MSVC build/test를 다시 확인하며, 구현 보고서의 RED 실패 증거를 대체하지 않는다. + ## Risks and Downstream Handoff ### Implementation Agent diff --git a/docs/releases/README.md b/docs/releases/README.md index 692828f..ba0c03d 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -43,7 +43,7 @@ GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT - Build/Test report status: `pass-for-reference-verification` - Implementation report와 implementation plan의 feature scope 일치 - requirements, formulations, numerical reviews, I/O definitions, reference models 문서의 feature scope 일치 -- validation command evidence: `python scripts/validate_workspace.py` +- Build/Test report의 `.harness/config.json` 또는 자동 감지 기반 MSVC build/test command evidence ## 문서 템플릿 @@ -99,8 +99,8 @@ GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT | command_or_report | expected | observed | notes | | --- | --- | --- | --- | -| python scripts/validate_workspace.py | pass | | | -| CMake/MSVC/CTest | pass | | | +| Build/Test report의 config-resolved CMake/MSVC/CTest | pass | | | +| Harness Python pytest (when applicable) | pass or N/A | | | | reference verification | pass-for-physics-evaluation | | | | physics evaluation | pass-for-release-agent | | | diff --git a/docs/superpowers/plans/2026-06-11-harden-execute-runner.md b/docs/superpowers/plans/2026-06-11-harden-execute-runner.md index 8df68b0..36e55ba 100644 --- a/docs/superpowers/plans/2026-06-11-harden-execute-runner.md +++ b/docs/superpowers/plans/2026-06-11-harden-execute-runner.md @@ -1,5 +1,9 @@ # Harden Execute Runner Implementation Plan +> **Historical / Superseded:** 이 계획의 `codex/` 브랜치, `allowed_paths`, explicit +> staging, 구형 workspace validation 계약은 현재 Runner 동작이 아니다. 현재 계약은 +> `docs/HARNESS_WORKFLOW.md`와 `docs/HARNESS.md`를 따른다. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make `scripts/execute.py` safe enough to use by enforcing `codex/` branch names, clean starting state, explicit staging, per-step file allowlists, and validation before every runner-created commit. diff --git a/docs/superpowers/plans/2026-06-11-reference-csv-comparison-contract.md b/docs/superpowers/plans/2026-06-11-reference-csv-comparison-contract.md index 147640b..986912f 100644 --- a/docs/superpowers/plans/2026-06-11-reference-csv-comparison-contract.md +++ b/docs/superpowers/plans/2026-06-11-reference-csv-comparison-contract.md @@ -1,5 +1,10 @@ # Reference CSV Comparison Contract Implementation Plan +> **Historical / Partially Superseded:** Reference CSV/HDF5 제품 계약의 배경 기록으로만 +> 보존한다. 이 문서의 Python `unittest`, 삭제된 workspace validation script, Harness +> 환경 변수 관련 검증 절차는 폐기되었으며 현재 운영 계약은 +> `docs/HARNESS_WORKFLOW.md`와 `docs/HARNESS.md`를 따른다. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Re-align FESA reference comparison contracts so FESA solver output remains authoritative HDF5, while Abaqus-derived reference results are CSV files compared directly against the FESA `results.h5` datasets. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5ee6477 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/reference/cantilever beam/cantilever beam displacements.csv b/reference/cantilever beam/cantilever beam displacements.csv new file mode 100644 index 0000000..24ae5db --- /dev/null +++ b/reference/cantilever beam/cantilever beam displacements.csv @@ -0,0 +1,12 @@ +Part Instance Name, Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3 +PART-1_1-1,1,0.000000E+00,0.000000E+00,-1.000000E-30,0.000000E+00,1.000000E-29,0.000000E+00 +PART-1_1-1,2,0.000000E+00,0.000000E+00,-2.900000E-04,0.000000E+00,5.428570E-04,0.000000E+00 +PART-1_1-1,3,0.000000E+00,0.000000E+00,-1.094290E-03,0.000000E+00,1.028570E-03,0.000000E+00 +PART-1_1-1,4,0.000000E+00,0.000000E+00,-2.355720E-03,0.000000E+00,1.457140E-03,0.000000E+00 +PART-1_1-1,5,0.000000E+00,0.000000E+00,-4.017140E-03,0.000000E+00,1.828570E-03,0.000000E+00 +PART-1_1-1,6,0.000000E+00,0.000000E+00,-6.021430E-03,0.000000E+00,2.142860E-03,0.000000E+00 +PART-1_1-1,7,0.000000E+00,0.000000E+00,-8.311430E-03,0.000000E+00,2.400000E-03,0.000000E+00 +PART-1_1-1,8,0.000000E+00,0.000000E+00,-1.083000E-02,0.000000E+00,2.600000E-03,0.000000E+00 +PART-1_1-1,9,0.000000E+00,0.000000E+00,-1.352000E-02,0.000000E+00,2.742860E-03,0.000000E+00 +PART-1_1-1,10,0.000000E+00,0.000000E+00,-1.632430E-02,0.000000E+00,2.828570E-03,0.000000E+00 +PART-1_1-1,11,0.000000E+00,0.000000E+00,-1.918570E-02,0.000000E+00,2.857140E-03,0.000000E+00 diff --git a/reference/cantilever beam/cantilever beam elemental forces.csv b/reference/cantilever beam/cantilever beam elemental forces.csv new file mode 100644 index 0000000..9720e64 --- /dev/null +++ b/reference/cantilever beam/cantilever beam elemental forces.csv @@ -0,0 +1,21 @@ +Part Instance Name, Element Label, Node Label, SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3,,,,, +PART-1_1-1,1,1,0.000000E+00,-1.000000E+06,0.000000E+00,9.500000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,1,2,0.000000E+00,-1.000000E+06,0.000000E+00,9.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,2,2,0.000000E+00,-1.000000E+06,0.000000E+00,9.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,2,3,0.000000E+00,-1.000000E+06,0.000000E+00,8.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,3,3,0.000000E+00,-1.000000E+06,0.000000E+00,8.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,3,4,0.000000E+00,-1.000000E+06,0.000000E+00,7.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,4,4,0.000000E+00,-1.000000E+06,0.000000E+00,7.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,4,5,0.000000E+00,-1.000000E+06,0.000000E+00,6.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,5,5,0.000000E+00,-1.000000E+06,0.000000E+00,6.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,5,6,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,6,6,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,6,7,0.000000E+00,-1.000000E+06,0.000000E+00,4.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,7,7,0.000000E+00,-1.000000E+06,0.000000E+00,4.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,7,8,0.000000E+00,-1.000000E+06,0.000000E+00,3.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,8,8,0.000000E+00,-1.000000E+06,0.000000E+00,3.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,8,9,0.000000E+00,-1.000000E+06,0.000000E+00,2.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,9,9,0.000000E+00,-1.000000E+06,0.000000E+00,2.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,9,10,0.000000E+00,-1.000000E+06,0.000000E+00,1.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,10,10,0.000000E+00,-1.000000E+06,0.000000E+00,1.000000E+06,0.000000E+00,0.000000E+00,,,,, +PART-1_1-1,10,11,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+05,0.000000E+00,0.000000E+00,,,,, diff --git a/reference/cantilever beam/cantilever beam reactions.csv b/reference/cantilever beam/cantilever beam reactions.csv new file mode 100644 index 0000000..83584a1 --- /dev/null +++ b/reference/cantilever beam/cantilever beam reactions.csv @@ -0,0 +1,12 @@ +Part Instance Name, Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3 +PART-1_1-1,1,0.000000E+00,0.000000E+00,1.000000E+06,0.000000E+00,-1.000000E+07,0.000000E+00 +PART-1_1-1,2,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,3,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,4,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,5,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,6,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,7,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,8,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,9,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,10,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 +PART-1_1-1,11,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00 diff --git a/reference/cantilever beam/cantilever beam.inp b/reference/cantilever beam/cantilever beam.inp new file mode 100644 index 0000000..9883654 --- /dev/null +++ b/reference/cantilever beam/cantilever beam.inp @@ -0,0 +1,104 @@ +*Heading +** Job name: Job-1 Model name: Model-1 +** Generated by: Abaqus/CAE Learning Edition 2024 +*Preprint, echo=NO, model=NO, history=NO, contact=NO +** +** PARTS +** +*Part, name=PART-1_1 +*Node + 1, 0., 0., 0. + 2, 1., 0., 0. + 3, 2., 0., 0. + 4, 3., 0., 0. + 5, 4., 0., 0. + 6, 5., 0., 0. + 7, 6., 0., 0. + 8, 7., 0., 0. + 9, 8., 0., 0. + 10, 9., 0., 0. + 11, 10., 0., 0. +*Element, type=B31 + 1, 1, 2 + 2, 2, 3 + 3, 3, 4 + 4, 4, 5 + 5, 5, 6 + 6, 6, 7 + 7, 7, 8 + 8, 8, 9 + 9, 9, 10 +10, 10, 11 +*Elset, elset=Set-1, generate + 1, 10, 1 +*Elset, elset=Set-2, generate + 1, 10, 1 +** Section: Section-1 Profile: Profile-1 +*Beam General Section, elset=Set-1, material=Material-1, section=GENERAL +1., 0.0833333, 0., 0.0833333, 0.140833 +0.,1.,0. +*Transverse Shear Stiffness +6.73077e+10, 6.73077e+10, 0.25 +*End Part +** +** +** ASSEMBLY +** +*Assembly, name=Assembly +** +*Instance, name=PART-1_1-1, part=PART-1_1 +*End Instance +** +*Nset, nset=Set-3, instance=PART-1_1-1 + 1, +*Nset, nset=Set-4, instance=PART-1_1-1 + 11, +*End Assembly +** +** MATERIALS +** +*Material, name=Material-1 +*Elastic + 2.1e+11, 0.3 +** +** BOUNDARY CONDITIONS +** +** Name: BC-1 Type: Displacement/Rotation +*Boundary +Set-3, 1, 1 +Set-3, 2, 2 +Set-3, 3, 3 +Set-3, 4, 4 +Set-3, 5, 5 +Set-3, 6, 6 +** ---------------------------------------------------------------- +** +** STEP: Step-1 +** +*Step, name=Step-1, nlgeom=NO +*Static +1., 1., 1e-05, 1. +** +** LOADS +** +** Name: Load-1 Type: Concentrated force +*Cload +Set-4, 3, -1e+06 +** +** OUTPUT REQUESTS +** +*Restart, write, frequency=0 +** +** FIELD OUTPUT: F-Output-1 +** +*Output, field +*Node Output +CF, RF, TF, U +*Element Output, directions=YES +LE, NFORC, NFORCSO, PE, PEEQ, PEMAG, S, SF +*Contact Output, variable=PRESELECT +** +** HISTORY OUTPUT: H-Output-1 +** +*Output, history, variable=PRESELECT +*End Step diff --git a/scripts/execute.py b/scripts/execute.py index 795266e..3ca984b 100644 --- a/scripts/execute.py +++ b/scripts/execute.py @@ -8,10 +8,8 @@ Usage: import argparse import contextlib -import fnmatch import json import os -import re import subprocess import sys import threading @@ -24,6 +22,10 @@ from typing import Optional ROOT = Path(__file__).resolve().parent.parent +class CodexEnvironmentError(RuntimeError): + """재시도로 해결할 수 없는 Codex CLI 환경 오류.""" + + @contextlib.contextmanager def progress_indicator(label: str): """터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다.""" @@ -56,10 +58,6 @@ class StepExecutor: """Phase 디렉토리 안의 step들을 순차 실행하는 하네스.""" MAX_RETRIES = 3 - VALIDATION_COMMANDS = ( - [sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"], - [sys.executable, "scripts/validate_workspace.py"], - ) FEAT_MSG = "feat({phase}): step {num} — {name}" CHORE_MSG = "chore({phase}): step {num} output" TZ = timezone(timedelta(hours=9)) @@ -89,7 +87,6 @@ class StepExecutor: def run(self): self._print_header() self._check_blockers() - self._assert_clean_worktree("before branch checkout") self._checkout_branch() guardrails = self._load_guardrails() self._ensure_created_at() @@ -117,117 +114,8 @@ class StepExecutor: cmd = ["git"] + list(args) return subprocess.run(cmd, cwd=self._root, capture_output=True, text=True) - def _validate_before_commit(self, commit_message: str): - print(f" Validation before commit: {commit_message}") - for cmd in self.VALIDATION_COMMANDS: - r = subprocess.run(cmd, cwd=self._root, capture_output=True, text=True) - if r.returncode != 0: - print(f" ERROR: validation failed before commit: {' '.join(cmd)}") - if r.stdout: - print(r.stdout[-2000:]) - if r.stderr: - print(r.stderr[-2000:]) - sys.exit(1) - - def _branch_name(self) -> str: - slug = re.sub(r"[^A-Za-z0-9._-]+", "-", self._phase_name.strip()) - slug = slug.strip("/.-") - if not slug: - slug = self._phase_dir_name - return f"codex/{slug}" - - def _assert_clean_worktree(self, context: str): - r = self._run_git("status", "--porcelain") - if r.returncode != 0: - print(" ERROR: git status failed.") - print(f" {r.stderr.strip()}") - sys.exit(1) - dirty = r.stdout.strip() - if dirty: - print(f" ERROR: dirty worktree detected {context}.") - print(" Commit, stash, or remove these changes before running scripts/execute.py:") - for line in dirty.splitlines(): - print(f" {line}") - sys.exit(1) - - @staticmethod - def _normalize_rel_path(path: str) -> str: - return path.replace("\\", "/").lstrip("./") - - def _path_allowed(self, path: str, patterns: list[str]) -> bool: - rel = self._normalize_rel_path(path) - for raw in patterns: - pattern = self._normalize_rel_path(str(raw)) - if not pattern: - continue - if pattern.endswith("/") and rel.startswith(pattern): - return True - if any(ch in pattern for ch in "*?[") and fnmatch.fnmatchcase(rel, pattern): - return True - if rel == pattern: - return True - return False - - def _validate_step_allowlist(self, step: dict): - allowed = step.get("allowed_paths") - if ( - not isinstance(allowed, list) - or not allowed - or not all(isinstance(p, str) and p.strip() for p in allowed) - ): - print(f" ERROR: Step {step.get('step')} must define non-empty allowed_paths.") - sys.exit(1) - - def _changed_paths(self) -> list[str]: - paths: list[str] = [] - tracked = self._run_git("diff", "--name-only") - if tracked.returncode != 0: - print(" ERROR: git diff --name-only failed.") - print(f" {tracked.stderr.strip()}") - sys.exit(1) - paths.extend(tracked.stdout.splitlines()) - - staged = self._run_git("diff", "--cached", "--name-only") - if staged.returncode != 0: - print(" ERROR: git diff --cached --name-only failed.") - print(f" {staged.stderr.strip()}") - sys.exit(1) - paths.extend(staged.stdout.splitlines()) - - untracked = self._run_git("ls-files", "--others", "--exclude-standard") - if untracked.returncode != 0: - print(" ERROR: git ls-files --others failed.") - print(f" {untracked.stderr.strip()}") - sys.exit(1) - paths.extend(untracked.stdout.splitlines()) - - return sorted({self._normalize_rel_path(p) for p in paths if p.strip()}) - - def _housekeeping_paths(self, step_num: int) -> set[str]: - return { - f"phases/{self._phase_dir_name}/index.json", - f"phases/{self._phase_dir_name}/step{step_num}-output.json", - "phases/index.json", - } - - def _classify_step_changes(self, step_num: int, step: dict, changed_paths: list[str]) -> tuple[list[str], list[str], list[str]]: - allowed_patterns = step.get("allowed_paths", []) - housekeeping_set = self._housekeeping_paths(step_num) - allowed: list[str] = [] - housekeeping: list[str] = [] - disallowed: list[str] = [] - for path in changed_paths: - rel = self._normalize_rel_path(path) - if rel in housekeeping_set: - housekeeping.append(rel) - elif self._path_allowed(rel, allowed_patterns): - allowed.append(rel) - else: - disallowed.append(rel) - return allowed, housekeeping, disallowed - def _checkout_branch(self): - branch = self._branch_name() + branch = f"feat-{self._phase_name}" r = self._run_git("rev-parse", "--abbrev-ref", "HEAD") if r.returncode != 0: @@ -249,45 +137,28 @@ class StepExecutor: print(f" Branch: {branch}") - def _stage_paths(self, paths: list[str]): - if not paths: - return - r = self._run_git("add", "--", *paths) - if r.returncode != 0: - print(" ERROR: git add failed.") - print(f" {r.stderr.strip()}") - sys.exit(1) + def _commit_step(self, step_num: int, step_name: str): + output_rel = f"phases/{self._phase_dir_name}/step{step_num}-output.json" + index_rel = f"phases/{self._phase_dir_name}/index.json" - def _commit_step(self, step: dict, step_name: str): - step_num = step["step"] - changed = self._changed_paths() - allowed, housekeeping, disallowed = self._classify_step_changes(step_num, step, changed) - if disallowed: - print(f" ERROR: Step {step_num} modified files outside allowed_paths:") - for path in disallowed: - print(f" {path}") - sys.exit(1) + self._run_git("add", "-A") + self._run_git("reset", "HEAD", "--", output_rel) + self._run_git("reset", "HEAD", "--", index_rel) - if allowed: + if self._run_git("diff", "--cached", "--quiet").returncode != 0: msg = self.FEAT_MSG.format(phase=self._phase_name, num=step_num, name=step_name) - self._validate_before_commit(msg) - self._stage_paths(allowed) - if self._run_git("diff", "--cached", "--quiet").returncode != 0: - r = self._run_git("commit", "-m", msg) - if r.returncode != 0: - print(f" ERROR: code commit failed: {r.stderr.strip()}") - sys.exit(1) + r = self._run_git("commit", "-m", msg) + if r.returncode == 0: print(f" Commit: {msg}") + else: + print(f" WARN: 코드 커밋 실패: {r.stderr.strip()}") - if housekeeping: + self._run_git("add", "-A") + if self._run_git("diff", "--cached", "--quiet").returncode != 0: msg = self.CHORE_MSG.format(phase=self._phase_name, num=step_num) - self._validate_before_commit(msg) - self._stage_paths(housekeeping) - if self._run_git("diff", "--cached", "--quiet").returncode != 0: - r = self._run_git("commit", "-m", msg) - if r.returncode != 0: - print(f" ERROR: housekeeping commit failed: {r.stderr.strip()}") - sys.exit(1) + r = self._run_git("commit", "-m", msg) + if r.returncode != 0: + print(f" WARN: housekeeping 커밋 실패: {r.stderr.strip()}") # --- top-level index --- @@ -311,11 +182,16 @@ class StepExecutor: sections = [] agents_md = ROOT / "AGENTS.md" if agents_md.exists(): - sections.append(f"## 프로젝트 규칙 (AGENTS.md)\n\n{agents_md.read_text(encoding='utf-8')}") + sections.append( + "## 프로젝트 규칙 (AGENTS.md)\n\n" + f"{agents_md.read_text(encoding='utf-8')}" + ) docs_dir = ROOT / "docs" if docs_dir.is_dir(): for doc in sorted(docs_dir.glob("*.md")): - sections.append(f"## {doc.stem}\n\n{doc.read_text(encoding='utf-8')}") + sections.append( + f"## {doc.stem}\n\n{doc.read_text(encoding='utf-8')}" + ) return "\n\n---\n\n".join(sections) if sections else "" @staticmethod @@ -330,11 +206,7 @@ class StepExecutor: return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n" def _build_preamble(self, guardrails: str, step_context: str, - allowed_paths: list[str], prev_error: Optional[str] = None) -> str: - commit_example = self.FEAT_MSG.format( - phase=self._phase_name, num="N", name="" - ) retry_section = "" if prev_error: retry_section = ( @@ -345,9 +217,6 @@ class StepExecutor: f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 step을 수행하세요.\n\n" f"{guardrails}\n\n---\n\n" f"{step_context}{retry_section}" - f"## Step file allowlist\n\n" - f"This step may modify only these repository-relative paths:\n" - f"{chr(10).join(f'- {p}' for p in allowed_paths)}\n\n" f"## 작업 규칙\n\n" f"1. 이전 step에서 작성된 코드를 확인하고 일관성을 유지하라.\n" f"2. 이 step에 명시된 작업만 수행하라. 추가 기능이나 파일을 만들지 마라.\n" @@ -357,8 +226,8 @@ class StepExecutor: f" - AC 통과 → \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n" f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 → \"error\" + \"error_message\" 기록\n" f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n" - f"6. 모든 변경사항을 커밋하라:\n" - f" {commit_example}\n\n---\n\n" + f"6. 변경사항을 직접 커밋하지 마라. Git 커밋과 timestamp는 실행기가 처리한다.\n\n" + f"---\n\n" ) # --- Codex 호출 --- @@ -372,10 +241,30 @@ class StepExecutor: sys.exit(1) prompt = preamble + step_file.read_text(encoding="utf-8") - result = subprocess.run( - ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "--json", prompt], - cwd=self._root, capture_output=True, text=True, timeout=1800, - ) + command = [ + "codex", + "exec", + "--json", + "--sandbox", + "workspace-write", + "--dangerously-bypass-hook-trust", + "--cd", + self._root, + "-", + ] + try: + result = subprocess.run( + command, + cwd=self._root, + input=prompt, + capture_output=True, + text=True, + timeout=1800, + ) + except FileNotFoundError as exc: + raise CodexEnvironmentError( + "Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요." + ) from exc if result.returncode != 0: print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})") @@ -388,11 +277,28 @@ class StepExecutor: "stdout": result.stdout, "stderr": result.stderr, } out_path = self._phase_dir / f"step{step_num}-output.json" - with open(out_path, "w", encoding="utf-8") as f: - json.dump(output, f, indent=2, ensure_ascii=False) + self._write_json(out_path, output) return output + @staticmethod + def _codex_environment_failure(output: dict) -> Optional[str]: + if output.get("exitCode", 0) == 0: + return None + diagnostic = ( + f"{output.get('stderr', '')}\n{output.get('stdout', '')}" + ).lower() + markers = { + "not logged in": "Codex 인증이 필요합니다.", + "authentication": "Codex 인증에 실패했습니다.", + "unexpected argument": "현재 Codex CLI가 필요한 옵션을 지원하지 않습니다.", + "unrecognized option": "현재 Codex CLI가 필요한 옵션을 지원하지 않습니다.", + } + for marker, message in markers.items(): + if marker in diagnostic: + return message + return None + # --- 헤더 & 검증 --- def _print_header(self): @@ -436,16 +342,20 @@ class StepExecutor: for attempt in range(1, self.MAX_RETRIES + 1): index = self._read_json(self._index_file) step_context = self._build_step_context(index) - preamble = self._build_preamble(guardrails, step_context, step.get("allowed_paths", []), prev_error) + preamble = self._build_preamble(guardrails, step_context, prev_error) tag = f"Step {step_num}/{self._total - 1} ({done} done): {step_name}" if attempt > 1: tag += f" [retry {attempt}/{self.MAX_RETRIES}]" with progress_indicator(tag) as pi: - self._invoke_codex(step, preamble) + output = self._invoke_codex(step, preamble) elapsed = int(pi.elapsed) + environment_failure = self._codex_environment_failure(output) + if environment_failure: + raise CodexEnvironmentError(environment_failure) + index = self._read_json(self._index_file) status = next((s.get("status", "pending") for s in index["steps"] if s["step"] == step_num), "pending") ts = self._stamp() @@ -455,7 +365,7 @@ class StepExecutor: if s["step"] == step_num: s["completed_at"] = ts self._write_json(self._index_file, index) - self._commit_step(step, step_name) + self._commit_step(step_num, step_name) print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]") return True @@ -470,9 +380,22 @@ class StepExecutor: self._update_top_index("blocked") sys.exit(2) + runtime_error = "" + if output["exitCode"] != 0: + runtime_error = ( + output.get("stderr", "").strip() + or output.get("stdout", "").strip() + ) err_msg = next( - (s.get("error_message", "Step did not update status") for s in index["steps"] if s["step"] == step_num), - "Step did not update status", + ( + s.get( + "error_message", + runtime_error or "Step did not update status", + ) + for s in index["steps"] + if s["step"] == step_num + ), + runtime_error or "Step did not update status", ) if attempt < self.MAX_RETRIES: @@ -490,7 +413,7 @@ class StepExecutor: s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}" s["failed_at"] = ts self._write_json(self._index_file, index) - self._commit_step(step, step_name) + self._commit_step(step_num, step_name) print(f" ✗ Step {step_num}: {step_name} failed after {self.MAX_RETRIES} attempts [{elapsed}s]") print(f" Error: {err_msg}") self._update_top_index("error") @@ -506,7 +429,6 @@ class StepExecutor: print("\n All steps completed!") return - self._validate_step_allowlist(pending) step_num = pending["step"] for s in index["steps"]: if s["step"] == step_num and "started_at" not in s: @@ -522,22 +444,15 @@ class StepExecutor: self._write_json(self._index_file, index) self._update_top_index("completed") - final_paths = [f"phases/{self._phase_dir_name}/index.json"] - if self._top_index_file.exists(): - final_paths.append("phases/index.json") - self._validate_before_commit(f"chore({self._phase_name}): mark phase completed") - self._stage_paths(final_paths) + self._run_git("add", "-A") if self._run_git("diff", "--cached", "--quiet").returncode != 0: msg = f"chore({self._phase_name}): mark phase completed" r = self._run_git("commit", "-m", msg) - if r.returncode != 0: - print(f" ERROR: phase completion commit failed: {r.stderr.strip()}") - sys.exit(1) - else: + if r.returncode == 0: print(f" ✓ {msg}") if self._auto_push: - branch = self._branch_name() + branch = f"feat-{self._phase_name}" r = self._run_git("push", "-u", "origin", branch) if r.returncode != 0: print(f"\n ERROR: git push 실패: {r.stderr.strip()}") @@ -555,7 +470,11 @@ def main(): parser.add_argument("--push", action="store_true", help="Push branch after completion") args = parser.parse_args() - StepExecutor(args.phase_dir, auto_push=args.push).run() + try: + StepExecutor(args.phase_dir, auto_push=args.push).run() + except CodexEnvironmentError as exc: + print(f"ERROR: {exc}") + sys.exit(1) if __name__ == "__main__": diff --git a/scripts/hooks/pre_tool_use.py b/scripts/hooks/pre_tool_use.py new file mode 100644 index 0000000..bca43ba --- /dev/null +++ b/scripts/hooks/pre_tool_use.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Codex PreToolUse 정책: 위험 명령과 테스트 없는 구현 파일 수정을 차단한다.""" + +import json +import re +import sys +from pathlib import Path +from typing import Any + +SCRIPTS_DIR = Path(__file__).resolve().parents[1] +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +from msvc_harness.config import ConfigError, load_config +from msvc_harness.tdd_policy import CPP_SUFFIXES, evaluate_paths + + +DANGEROUS_PATTERNS = ( + re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE), + re.compile(r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b", re.IGNORECASE), + re.compile(r"\brm\s+-rf\b", re.IGNORECASE), + re.compile( + r"\bRemove-Item\b(?=[^\n]*-Recurse\b)(?=[^\n]*-Force\b)", + re.IGNORECASE, + ), + re.compile( + r"\b(?:rmdir|rd)\b(?=[^\n]*/s\b)(?=[^\n]*/q\b)", + re.IGNORECASE, + ), + re.compile(r"\bDROP\s+TABLE\b", re.IGNORECASE), +) +PATCH_PATH = re.compile( + r"^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:) (?P.+?)\s*$", + re.MULTILINE, +) + + +def _tool_input(payload: dict[str, Any]) -> dict[str, Any]: + value = payload.get("tool_input", {}) + return value if isinstance(value, dict) else {} + + +def _candidate_paths(payload: dict[str, Any], root: Path) -> list[Path]: + tool_input = _tool_input(payload) + raw_paths: list[str] = [] + for key in ("path", "file_path"): + value = tool_input.get(key) + if isinstance(value, str) and value.strip(): + raw_paths.append(value.strip()) + + edits = tool_input.get("edits") + if isinstance(edits, list): + for edit in edits: + if not isinstance(edit, dict): + continue + value = edit.get("path") + if isinstance(value, str) and value.strip(): + raw_paths.append(value.strip()) + + for key in ("patch", "input"): + value = tool_input.get(key) + if isinstance(value, str): + raw_paths.extend(match.group("path") for match in PATCH_PATH.finditer(value)) + + paths: list[Path] = [] + for raw_path in raw_paths: + path = Path(raw_path) + paths.append(path.resolve() if path.is_absolute() else (root / path).resolve()) + return list(dict.fromkeys(paths)) + + +def evaluate(payload: dict[str, Any], root: Path) -> str | None: + """Return a blocking reason, or None when the tool call is allowed.""" + tool_name = str(payload.get("tool_name", "")) + tool_input = _tool_input(payload) + + if tool_name in {"Bash", "shell_command", "PowerShell"}: + command = tool_input.get("command", "") + if isinstance(command, str) and any( + pattern.search(command) for pattern in DANGEROUS_PATTERNS + ): + return "위험한 명령어가 감지되어 실행을 차단했습니다." + + if tool_name not in {"apply_patch", "Edit", "MultiEdit", "Write"}: + return None + + paths = _candidate_paths(payload, root) + try: + config = load_config(root) + except ConfigError as exc: + if any(path.suffix.lower() in CPP_SUFFIXES for path in paths): + return f"TDD GUARD: .harness/config.json must be repaired: {exc}" + return None + return evaluate_paths(paths, root, config.tdd) + + +def main() -> int: + try: + payload = json.load(sys.stdin) + if not isinstance(payload, dict): + raise TypeError("hook input must be a JSON object") + cwd = payload.get("cwd") + if cwd is not None and not isinstance(cwd, str): + raise TypeError("hook cwd must be a string") + except (json.JSONDecodeError, TypeError) as exc: + print(f"TDD GUARD: hook 입력을 해석하지 못해 검사를 건너뜁니다: {exc}", file=sys.stderr) + return 0 + + root = Path(payload.get("cwd") or Path.cwd()).resolve() + reason = evaluate(payload, root) + if reason: + print(reason, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hooks/stop_validation.py b/scripts/hooks/stop_validation.py new file mode 100644 index 0000000..5c58c8c --- /dev/null +++ b/scripts/hooks/stop_validation.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Codex Stop hook: validate discovered C/C++ projects with MSVC.""" + +import json +import os +import subprocess +import sys +import time +from pathlib import Path + + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) + +from msvc_harness.adapters.base import AdapterError +from msvc_harness.adapters.cmake import CMakeAdapter +from msvc_harness.adapters.msbuild import MSBuildAdapter +from msvc_harness.config import ConfigError, load_config +from msvc_harness.discovery import DiscoveryError, discover_project +from msvc_harness.models import ProjectKind +from msvc_harness.process import ValidationFailure, execute_plan +from msvc_harness.toolchain import ToolchainError, discover_toolchain + + +REENTRY_ENV = "CODEX_STOP_VALIDATION_ACTIVE" +TOTAL_TIMEOUT_SECONDS = 1800 + + +def _remaining(deadline, clock, stage): + remaining = deadline - clock() + if remaining <= 0: + raise ValidationFailure(f"{stage} timed out before it could start") + return remaining + + +def _project_root( + cwd: Path, + *, + deadline: float, + run=subprocess.run, + clock=time.monotonic, +) -> Path: + argv = ["git", "rev-parse", "--show-toplevel"] + resolved_cwd = cwd.resolve() + try: + result = run( + argv, + cwd=resolved_cwd, + capture_output=True, + text=True, + shell=False, + timeout=_remaining(deadline, clock, "repository discovery"), + ) + except subprocess.TimeoutExpired as exc: + raise ValidationFailure( + f"repository discovery timed out; argv={argv!r}; " + f"cwd={str(resolved_cwd)!r}" + ) from exc + if result.returncode == 0 and result.stdout.strip(): + return Path(result.stdout.strip()).resolve() + return resolved_cwd + + +def run_validations( + root: Path, + *, + deadline: float | None = None, + clock=time.monotonic, +) -> tuple[bool, str]: + """Build and test a discovered C/C++ project, when one exists.""" + if deadline is None: + deadline = clock() + TOTAL_TIMEOUT_SECONDS + try: + config = load_config(root) + discovery = discover_project(root, config) + if discovery.selection is None: + return True, "" + selection = discovery.selection + tools = discover_toolchain( + selection.kind, + deadline=deadline, + clock=clock, + ) + adapter = ( + CMakeAdapter() + if selection.kind is ProjectKind.CMAKE + else MSBuildAdapter() + ) + plan = adapter.create_plan(root, selection, config, tools) + child_env = os.environ.copy() + child_env[REENTRY_ENV] = "1" + execute_plan( + plan, + root, + env=child_env, + deadline=deadline, + clock=clock, + ) + return True, "" + except ( + ConfigError, + DiscoveryError, + ToolchainError, + AdapterError, + ValidationFailure, + OSError, + ) as exc: + return False, str(exc) + + +def _emit_stop_response(message: str) -> None: + print( + json.dumps( + { + "continue": False, + "stopReason": message, + "systemMessage": message, + }, + ensure_ascii=False, + ) + ) + + +def main(*, run=subprocess.run, clock=time.monotonic) -> int: + if os.environ.get(REENTRY_ENV) == "1": + return 0 + + deadline = clock() + TOTAL_TIMEOUT_SECONDS + try: + root = _project_root( + Path.cwd(), + deadline=deadline, + run=run, + clock=clock, + ) + ok, message = run_validations(root, deadline=deadline, clock=clock) + except (OSError, ValidationFailure) as exc: + _emit_stop_response(f"validation hook failed: {exc}") + return 0 + + if ok: + return 0 + _emit_stop_response(message) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/msvc_harness/__init__.py b/scripts/msvc_harness/__init__.py new file mode 100644 index 0000000..c59cfff --- /dev/null +++ b/scripts/msvc_harness/__init__.py @@ -0,0 +1,3 @@ +from .config import ConfigError, load_config + +__all__ = ["ConfigError", "load_config"] diff --git a/scripts/msvc_harness/adapters/__init__.py b/scripts/msvc_harness/adapters/__init__.py new file mode 100644 index 0000000..c343612 --- /dev/null +++ b/scripts/msvc_harness/adapters/__init__.py @@ -0,0 +1,5 @@ +from .base import AdapterError, ValidationAdapter +from .cmake import CMakeAdapter +from .msbuild import MSBuildAdapter + +__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"] diff --git a/scripts/msvc_harness/adapters/base.py b/scripts/msvc_harness/adapters/base.py new file mode 100644 index 0000000..0c97c4e --- /dev/null +++ b/scripts/msvc_harness/adapters/base.py @@ -0,0 +1,19 @@ +from pathlib import Path +from typing import Protocol + +from ..models import HarnessConfig, ProjectSelection, Toolchain, ValidationPlan + + +class AdapterError(RuntimeError): + pass + + +class ValidationAdapter(Protocol): + def create_plan( + self, + root: Path, + selection: ProjectSelection, + config: HarnessConfig, + tools: Toolchain, + ) -> ValidationPlan: + raise NotImplementedError diff --git a/scripts/msvc_harness/adapters/cmake.py b/scripts/msvc_harness/adapters/cmake.py new file mode 100644 index 0000000..f7d449d --- /dev/null +++ b/scripts/msvc_harness/adapters/cmake.py @@ -0,0 +1,99 @@ +from pathlib import Path + +from ..models import ( + CommandSpec, + HarnessConfig, + ProjectKind, + ProjectSelection, + ResultCheck, + ResultCheckKind, + Toolchain, + ValidationPlan, + ValidationStep, +) +from .base import AdapterError + + +class CMakeAdapter: + def create_plan( + self, + root: Path, + selection: ProjectSelection, + config: HarnessConfig, + tools: Toolchain, + ) -> ValidationPlan: + if selection.kind is not ProjectKind.CMAKE: + raise AdapterError("CMakeAdapter requires a CMake project selection") + if tools.cmake is None: + raise AdapterError("CMake tool is required") + if tools.ctest is None: + raise AdapterError("CTest tool is required") + + if config.cmake.configure_preset is None: + command_cwd = root + binary = root / ".harness/build" + configure = ( + str(tools.cmake), + "-S", + str(config.cmake.source_dir), + "-B", + str(binary), + "-A", + "x64", + ) + build = (str(tools.cmake), "--build", str(binary), "--config", "Debug") + discover = ( + str(tools.ctest), + "--test-dir", + str(binary), + "-C", + "Debug", + "--show-only=json-v1", + ) + test = ( + str(tools.ctest), + "--test-dir", + str(binary), + "-C", + "Debug", + "--output-on-failure", + ) + else: + command_cwd = config.cmake.source_dir + binary = config.cmake.binary_dir + if ( + binary is None + or config.cmake.build_preset is None + or config.cmake.test_preset is None + ): + raise AdapterError("CMake presets require binary, build, and test settings") + configure = (str(tools.cmake), "--preset", config.cmake.configure_preset) + build = (str(tools.cmake), "--build", "--preset", config.cmake.build_preset) + discover = ( + str(tools.ctest), + "--preset", + config.cmake.test_preset, + "--show-only=json-v1", + ) + test = ( + str(tools.ctest), + "--preset", + config.cmake.test_preset, + "--output-on-failure", + ) + + return ValidationPlan( + ProjectKind.CMAKE, + ( + ValidationStep( + CommandSpec(configure, command_cwd, "configure"), + (ResultCheck(ResultCheckKind.CMAKE_COMPILER_IS_MSVC, binary),), + ), + ValidationStep(CommandSpec(build, command_cwd, "build")), + ValidationStep( + CommandSpec(discover, command_cwd, "test-discovery"), + (ResultCheck(ResultCheckKind.CTEST_HAS_TESTS),), + ), + ValidationStep(CommandSpec(test, command_cwd, "test")), + ), + ) diff --git a/scripts/msvc_harness/adapters/msbuild.py b/scripts/msvc_harness/adapters/msbuild.py new file mode 100644 index 0000000..00c95e0 --- /dev/null +++ b/scripts/msvc_harness/adapters/msbuild.py @@ -0,0 +1,60 @@ +from pathlib import Path + +from ..models import ( + CommandSpec, + HarnessConfig, + ProjectKind, + ProjectSelection, + Toolchain, + ValidationPlan, + ValidationStep, +) +from .base import AdapterError + + +def _test_argv(root: Path, command: tuple[str, ...]) -> tuple[str, ...]: + if "/" not in command[0] and "\\" not in command[0]: + return command + first = Path(command[0].replace("\\", "/")) + executable = first if first.is_absolute() else (root / first).resolve() + try: + executable.relative_to(root) + except ValueError as exc: + raise AdapterError( + "msbuild.testCommand resolves outside the repository" + ) from exc + return (str(executable), *command[1:]) + + +class MSBuildAdapter: + def create_plan( + self, + root: Path, + selection: ProjectSelection, + config: HarnessConfig, + tools: Toolchain, + ) -> ValidationPlan: + if selection.kind is not ProjectKind.MSBUILD: + raise AdapterError("MSBuildAdapter received a non-MSBuild project") + if not config.msbuild.test_command: + raise AdapterError( + "msbuild.testCommand is required for .sln/.vcxproj validation" + ) + build = ValidationStep( + CommandSpec( + ( + str(tools.msbuild), + str(selection.project_file), + "/m", + "/nologo", + f"/p:Configuration={config.msbuild.configuration}", + f"/p:Platform={config.msbuild.platform}", + ), + root, + "build", + ) + ) + test = ValidationStep( + CommandSpec(_test_argv(root, config.msbuild.test_command), root, "test") + ) + return ValidationPlan(ProjectKind.MSBUILD, (build, test)) diff --git a/scripts/msvc_harness/config.py b/scripts/msvc_harness/config.py new file mode 100644 index 0000000..dd9ef43 --- /dev/null +++ b/scripts/msvc_harness/config.py @@ -0,0 +1,146 @@ +import json +from pathlib import Path +from typing import Any + +from .models import CMakeConfig, HarnessConfig, MSBuildConfig, TDDConfig + +DEFAULT_TEST_PATTERNS = ( + "{stem}_test.cpp", + "{stem}_tests.cpp", + "test_{stem}.cpp", + "{stem}.test.cpp", +) + + +class ConfigError(ValueError): + pass + + +def _reject_unknown(data: dict[str, Any], allowed: set[str], label: str) -> None: + unknown = sorted(set(data) - allowed) + if unknown: + raise ConfigError(f"{label} contains unexpected field: {unknown[0]}") + + +def _repo_path(root: Path, raw: str, label: str) -> Path: + candidate = Path(raw) + if candidate.is_absolute(): + raise ConfigError(f"{label} must be repository-relative") + resolved = (root / candidate).resolve() + try: + resolved.relative_to(root) + except ValueError as exc: + raise ConfigError(f"{label} resolves outside the repository") from exc + return resolved + + +def _mapping(value: Any, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise ConfigError(f"{label} must be an object") + return value + + +def _nonempty_string(value: Any, label: str) -> str: + if not isinstance(value, str) or not value: + raise ConfigError(f"{label} must be a non-empty string") + return value + + +def _string_array(value: Any, label: str) -> tuple[str, ...]: + if not isinstance(value, list): + raise ConfigError(f"{label} must be an array") + if not value or any(not isinstance(item, str) or not item for item in value): + raise ConfigError(f"{label} must be a non-empty string array") + return tuple(value) + + +def load_config(root: Path) -> HarnessConfig: + root = root.resolve() + config_path = root / ".harness" / "config.json" + if config_path.is_file(): + try: + data = json.loads(config_path.read_text(encoding="utf-8")) + except UnicodeDecodeError as exc: + raise ConfigError( + f"{config_path}: configuration must be valid UTF-8: {exc}" + ) from exc + except json.JSONDecodeError as exc: + raise ConfigError(f"{config_path}: {exc}") from exc + else: + data = {"version": 1} + + data = _mapping(data, "config") + _reject_unknown(data, {"version", "projectType", "cmake", "msbuild", "tdd"}, "config") + + if data.get("version") != 1 or isinstance(data.get("version"), bool): + raise ConfigError("version must be 1") + project_type = data.get("projectType", "auto") + if project_type not in {"auto", "cmake", "msbuild"}: + raise ConfigError("projectType must be auto, cmake, or msbuild") + + cmake_data = _mapping(data.get("cmake", {}), "cmake") + _reject_unknown( + cmake_data, + {"sourceDir", "binaryDir", "configurePreset", "buildPreset", "testPreset"}, + "cmake", + ) + source_dir = _repo_path( + root, _nonempty_string(cmake_data.get("sourceDir", "."), "cmake.sourceDir"), "cmake.sourceDir" + ) + binary_dir = None + if "binaryDir" in cmake_data: + binary_dir = _repo_path( + root, _nonempty_string(cmake_data["binaryDir"], "cmake.binaryDir"), "cmake.binaryDir" + ) + preset_keys = ("configurePreset", "buildPreset", "testPreset", "binaryDir") + if any(key in cmake_data for key in preset_keys) and not all(key in cmake_data for key in preset_keys): + raise ConfigError("cmake configurePreset, buildPreset, testPreset, and binaryDir must be specified together") + configure_preset = ( + _nonempty_string(cmake_data["configurePreset"], "cmake.configurePreset") + if "configurePreset" in cmake_data + else None + ) + build_preset = ( + _nonempty_string(cmake_data["buildPreset"], "cmake.buildPreset") + if "buildPreset" in cmake_data + else None + ) + test_preset = ( + _nonempty_string(cmake_data["testPreset"], "cmake.testPreset") + if "testPreset" in cmake_data + else None + ) + + msbuild_data = _mapping(data.get("msbuild", {}), "msbuild") + _reject_unknown(msbuild_data, {"solution", "configuration", "platform", "testCommand"}, "msbuild") + solution = None + if "solution" in msbuild_data: + solution = _repo_path( + root, _nonempty_string(msbuild_data["solution"], "msbuild.solution"), "msbuild.solution" + ) + configuration = _nonempty_string(msbuild_data.get("configuration", "Debug"), "msbuild.configuration") + platform = _nonempty_string(msbuild_data.get("platform", "x64"), "msbuild.platform") + test_command = ( + _string_array(msbuild_data["testCommand"], "msbuild.testCommand") + if "testCommand" in msbuild_data + else None + ) + + tdd_data = _mapping(data.get("tdd", {}), "tdd") + _reject_unknown(tdd_data, {"testRoots", "testPatterns", "exclude"}, "tdd") + raw_test_roots = _string_array(tdd_data["testRoots"], "tdd.testRoots") if "testRoots" in tdd_data else ("tests", "test") + test_roots = tuple(_repo_path(root, item, "tdd.testRoots") for item in raw_test_roots) + test_patterns = _string_array(tdd_data["testPatterns"], "tdd.testPatterns") if "testPatterns" in tdd_data else DEFAULT_TEST_PATTERNS + if any("{stem}" not in pattern for pattern in test_patterns): + raise ConfigError("every tdd.testPatterns entry must contain {stem}") + exclude = _string_array(tdd_data["exclude"], "tdd.exclude") if "exclude" in tdd_data else () + for item in exclude: + _repo_path(root, item, "tdd.exclude") + + return HarnessConfig( + version=1, + project_type=project_type, + cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset), + msbuild=MSBuildConfig(solution, configuration, platform, test_command), + tdd=TDDConfig(test_roots, test_patterns, exclude), + ) diff --git a/scripts/msvc_harness/discovery.py b/scripts/msvc_harness/discovery.py new file mode 100644 index 0000000..282c69d --- /dev/null +++ b/scripts/msvc_harness/discovery.py @@ -0,0 +1,77 @@ +from pathlib import Path + +from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection +from .tdd_policy import find_cpp_files + + +class DiscoveryError(RuntimeError): + pass + + +def _cmake_entry(source: Path) -> Path | None: + for name in ("CMakePresets.json", "CMakeUserPresets.json", "CMakeLists.txt"): + candidate = source / name + if candidate.is_file(): + return candidate + return None + + +def _single_msbuild(root: Path, explicit: Path | None) -> Path: + if explicit is not None: + if explicit.is_file() and explicit.suffix.lower() in {".sln", ".vcxproj"}: + return explicit + raise DiscoveryError(f"configured MSBuild project is invalid: {explicit}") + solutions = sorted(root.glob("*.sln")) + if len(solutions) > 1: + raise DiscoveryError("multiple .sln files found; set msbuild.solution") + if solutions: + return solutions[0] + projects = sorted(root.glob("*.vcxproj")) + if len(projects) > 1: + raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution") + if projects: + return projects[0] + raise DiscoveryError("no root .sln or .vcxproj was found") + + +def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult: + root = root.resolve() + if config.project_type == "cmake": + entry = _cmake_entry(config.cmake.source_dir) + if entry is None: + raise DiscoveryError( + f"no CMake project found in {config.cmake.source_dir}" + ) + return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry)) + + if config.project_type == "msbuild": + project = _single_msbuild(root, config.msbuild.solution) + return DiscoveryResult(ProjectSelection(ProjectKind.MSBUILD, project)) + + entry = _cmake_entry(root) + if entry is not None: + return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry)) + + solutions = sorted(root.glob("*.sln")) + if len(solutions) > 1: + raise DiscoveryError("multiple .sln files found; set msbuild.solution") + if solutions: + return DiscoveryResult( + ProjectSelection(ProjectKind.MSBUILD, solutions[0]) + ) + + projects = sorted(root.glob("*.vcxproj")) + if len(projects) > 1: + raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution") + if projects: + return DiscoveryResult( + ProjectSelection(ProjectKind.MSBUILD, projects[0]) + ) + + cpp_files = find_cpp_files(root) + if cpp_files: + raise DiscoveryError( + "C/C++ files exist but no CMakeLists.txt, preset, .sln, or .vcxproj " + "was found; configure projectType and project path" + ) + return DiscoveryResult(None, ()) diff --git a/scripts/msvc_harness/models.py b/scripts/msvc_harness/models.py new file mode 100644 index 0000000..1c03085 --- /dev/null +++ b/scripts/msvc_harness/models.py @@ -0,0 +1,100 @@ +from dataclasses import dataclass +from enum import Enum +from pathlib import Path + + +class ProjectKind(Enum): + CMAKE = "cmake" + MSBUILD = "msbuild" + + +class ResultCheckKind(Enum): + CMAKE_COMPILER_IS_MSVC = "cmake_compiler_is_msvc" + CTEST_HAS_TESTS = "ctest_has_tests" + + +@dataclass(frozen=True) +class CMakeConfig: + source_dir: Path + binary_dir: Path | None = None + configure_preset: str | None = None + build_preset: str | None = None + test_preset: str | None = None + + +@dataclass(frozen=True) +class MSBuildConfig: + solution: Path | None = None + configuration: str = "Debug" + platform: str = "x64" + test_command: tuple[str, ...] | None = None + + +@dataclass(frozen=True) +class TDDConfig: + test_roots: tuple[Path, ...] + test_patterns: tuple[str, ...] + exclude: tuple[str, ...] = () + + +@dataclass(frozen=True) +class HarnessConfig: + version: int + project_type: str + cmake: CMakeConfig + msbuild: MSBuildConfig + tdd: TDDConfig + + +@dataclass(frozen=True) +class ProjectSelection: + kind: ProjectKind + project_file: Path + + +@dataclass(frozen=True) +class DiscoveryResult: + selection: ProjectSelection | None + cpp_files: tuple[Path, ...] = () + + +@dataclass(frozen=True) +class Toolchain: + installation: Path + msbuild: Path + cmake: Path | None = None + ctest: Path | None = None + + +@dataclass(frozen=True) +class CommandSpec: + argv: tuple[str, ...] + cwd: Path + stage: str + timeout_seconds: int = 1800 + + +@dataclass(frozen=True) +class ResultCheck: + kind: ResultCheckKind + path: Path | None = None + + +@dataclass(frozen=True) +class ValidationStep: + command: CommandSpec + checks: tuple[ResultCheck, ...] = () + + +@dataclass(frozen=True) +class ValidationPlan: + project_kind: ProjectKind + steps: tuple[ValidationStep, ...] + + +@dataclass(frozen=True) +class CommandResult: + command: CommandSpec + returncode: int + stdout: str + stderr: str diff --git a/scripts/msvc_harness/process.py b/scripts/msvc_harness/process.py new file mode 100644 index 0000000..0911b13 --- /dev/null +++ b/scripts/msvc_harness/process.py @@ -0,0 +1,140 @@ +import json +import locale +import re +import subprocess +import time + +from .models import CommandResult, ResultCheckKind + +DIAGNOSTIC_LIMIT = 8000 + + +class ValidationFailure(RuntimeError): + pass + + +def _decode(value): + if value is None: + return "" + if isinstance(value, str): + return value + for encoding in ("utf-8", locale.getpreferredencoding(False)): + try: + return value.decode(encoding) + except (UnicodeDecodeError, LookupError): + continue + return value.decode("utf-8", errors="replace") + + +def _check_result(check, result): + if check.kind is ResultCheckKind.CMAKE_COMPILER_IS_MSVC: + if check.path is None: + raise ValidationFailure("MSVC check is missing binaryDir") + files = sorted(check.path.glob("CMakeFiles/*/CMakeCXXCompiler.cmake")) + if not files: + raise ValidationFailure("CMake did not generate compiler metadata") + text = files[-1].read_text(encoding="utf-8", errors="replace") + if not re.search( + r'set\s*\(\s*CMAKE_CXX_COMPILER_ID\s+"MSVC"\s*\)', text + ): + raise ValidationFailure("CMake selected a compiler other than MSVC") + elif check.kind is ResultCheckKind.CTEST_HAS_TESTS: + try: + payload = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise ValidationFailure("CTest discovery did not return JSON") from exc + if not isinstance(payload, dict): + raise ValidationFailure("CTest discovery JSON must be an object") + if not isinstance(payload.get("tests"), list) or not payload["tests"]: + raise ValidationFailure("CTest discovered no tests") + + +def _diagnostic(stdout, stderr): + diagnostic = (stdout + "\n" + stderr).rstrip()[-DIAGNOSTIC_LIMIT:] + return diagnostic.replace("\r\n", " | ").replace("\n", " | ") + + +def _command_context(argv, cwd): + return f"argv={list(argv)!r}; cwd={str(cwd)!r}" + + +def execute_plan( + plan, + root, + *, + env=None, + total_timeout_seconds=1800, + deadline=None, + run=subprocess.run, + clock=time.monotonic, +): + root = root.resolve() + if deadline is None: + deadline = clock() + total_timeout_seconds + results = [] + + for step in plan.steps: + cwd = step.command.cwd.resolve() + context = _command_context(step.command.argv, cwd) + try: + cwd.relative_to(root) + except ValueError as exc: + raise ValidationFailure( + f"{step.command.stage} working directory is outside the repository; " + f"{context}" + ) from exc + + remaining = deadline - clock() + if remaining <= 0: + raise ValidationFailure( + f"{step.command.stage} timed out before it could start; {context}" + ) + + timeout = min(step.command.timeout_seconds, remaining) + try: + completed = run( + list(step.command.argv), + cwd=cwd, + env=env, + capture_output=True, + shell=False, + timeout=timeout, + ) + except subprocess.TimeoutExpired as exc: + stdout = _decode(exc.output) + stderr = _decode(exc.stderr) + diagnostic = _diagnostic(stdout, stderr) + message = ( + f"{step.command.stage} timed out after {timeout} seconds; {context}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ValidationFailure(message) from exc + + stdout = _decode(completed.stdout) + stderr = _decode(completed.stderr) + result = CommandResult(step.command, completed.returncode, stdout, stderr) + if completed.returncode != 0: + diagnostic = _diagnostic(stdout, stderr) + message = ( + f"{step.command.stage} failed with exit code {completed.returncode}; " + f"{context}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ValidationFailure(message) + + try: + for check in step.checks: + _check_result(check, result) + except ValidationFailure as exc: + diagnostic = _diagnostic(stdout, stderr) + message = ( + f"{step.command.stage} result check failed: {exc}; {context}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ValidationFailure(message) from exc + results.append(result) + + return tuple(results) diff --git a/scripts/msvc_harness/tdd_policy.py b/scripts/msvc_harness/tdd_policy.py new file mode 100644 index 0000000..b5f5b8c --- /dev/null +++ b/scripts/msvc_harness/tdd_policy.py @@ -0,0 +1,88 @@ +from fnmatch import fnmatch +from pathlib import Path + +CPP_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx"}) +DEFAULT_EXCLUDES = ( + ".harness/build/**", + "build/**", + "out/**", + "cmake-build-*/**", + "third_party/**", + "external/**", + "vendor/**", + "generated/**", +) + + +def is_excluded_path(path: Path, root: Path, extra: tuple[str, ...] = ()) -> bool: + try: + relative = path.resolve().relative_to(root.resolve()).as_posix() + except ValueError: + return False + return any(fnmatch(relative, pattern) for pattern in DEFAULT_EXCLUDES + extra) + + +def find_cpp_files( + root: Path, + extra_excludes: tuple[str, ...] = (), +) -> tuple[Path, ...]: + found = ( + path + for path in root.resolve().rglob("*") + if path.is_file() + and path.suffix.lower() in CPP_SUFFIXES + and not is_excluded_path(path, root, extra_excludes) + ) + return tuple(sorted(found)) + + +def is_test_file(path: Path, root: Path, config) -> bool: + resolved = path.resolve() + if any(root == resolved or root in resolved.parents for root in config.test_roots): + return True + relative = resolved.relative_to(root.resolve()) + if {"test", "tests"} & {part.lower() for part in relative.parts}: + return True + stem = resolved.stem.lower() + return stem.startswith("test_") or stem.endswith(("_test", "_tests", ".test")) + + +def matching_test_exists(path: Path, config) -> bool: + names = tuple(pattern.format(stem=path.stem) for pattern in config.test_patterns) + roots = ( + *config.test_roots, + path.parent / "tests", + path.parent / "test", + ) + for root in roots: + if not root.is_dir(): + continue + for name in names: + if any(candidate.is_file() for candidate in root.rglob(name)): + return True + return False + + +def evaluate_paths(paths, root: Path, config) -> str | None: + root = root.resolve() + for raw in paths: + path = raw.resolve() + try: + path.relative_to(root) + except ValueError: + return f"TDD GUARD: '{path}' is outside the repository" + if path.suffix.lower() not in CPP_SUFFIXES: + continue + if ( + path.name.lower() == "main.cpp" + or is_excluded_path(path, root, config.exclude) + or is_test_file(path, root, config) + ): + continue + if not matching_test_exists(path, config): + expected = config.test_patterns[0].format(stem=path.stem) + return ( + f"TDD GUARD: '{path.name}' requires an existing test such as " + f"'{expected}'. Add the test in a configured test root first." + ) + return None diff --git a/scripts/msvc_harness/toolchain.py b/scripts/msvc_harness/toolchain.py new file mode 100644 index 0000000..b1d63d0 --- /dev/null +++ b/scripts/msvc_harness/toolchain.py @@ -0,0 +1,169 @@ +import os +import shutil +import subprocess +import time +from pathlib import Path + +from .models import ProjectKind, Toolchain + +VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" +VS_CMAKE_RELATIVE = Path( + "Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin" +) + + +class ToolchainError(RuntimeError): + pass + + +def _decode(value): + if isinstance(value, str): + return value + return (value or b"").decode("utf-8", errors="replace") + + +def _first_path(result, label, argv, cwd): + text = _decode(result.stdout).strip() + if result.returncode != 0: + diagnostic = ( + _decode(result.stdout) + "\n" + _decode(result.stderr) + ).rstrip()[-8000:] + diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ") + message = ( + f"{label} probe failed with exit code {result.returncode}; " + f"argv={argv!r}; cwd={str(cwd)!r}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ToolchainError( + f"{message}; install Visual Studio Desktop development with C++" + ) + if not text: + raise ToolchainError( + f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; " + "install Visual Studio Desktop development with C++" + ) + path = Path(text.splitlines()[0]).resolve() + if not path.exists(): + diagnostic = ( + _decode(result.stdout) + "\n" + _decode(result.stderr) + ).rstrip()[-8000:] + diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ") + message = ( + f"{label} probe returned nonexistent path with exit code 0; " + f"argv={argv!r}; cwd={str(cwd)!r}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ToolchainError( + f"{message}; install or repair Visual Studio Desktop development with C++" + ) + return path + + +def _require_path(path, label): + if path is None or not path.exists(): + raise ToolchainError(f"{label} not found") + return path.resolve() + + +def _remaining(deadline, clock, label): + if deadline is None: + return None + remaining = deadline - clock() + if remaining <= 0: + raise ToolchainError(f"{label} probe timed out before it could start") + return remaining + + +def _probe(run, argv, label, *, deadline, clock): + cwd = Path.cwd().resolve() + kwargs = { + "cwd": cwd, + "capture_output": True, + "check": False, + "shell": False, + } + remaining = _remaining(deadline, clock, label) + if remaining is not None: + kwargs["timeout"] = remaining + try: + return run(argv, **kwargs) + except subprocess.TimeoutExpired as exc: + raise ToolchainError( + f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; " + "install or repair Visual Studio Desktop development with C++" + ) from exc + + +def discover_toolchain( + kind: ProjectKind, + *, + env=None, + which=shutil.which, + run=subprocess.run, + deadline=None, + clock=time.monotonic, +) -> Toolchain: + environment = os.environ if env is None else env + vswhere_path = which("vswhere.exe") + if vswhere_path is None: + program_files = environment.get("ProgramFiles(x86)") + if program_files: + candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe" + vswhere_path = str(candidate) if candidate.exists() else None + vswhere = _require_path( + Path(vswhere_path) if vswhere_path else None, + "vswhere.exe; install Visual Studio Desktop development with C++", + ) + + install_argv = [ + str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD, + "-property", "installationPath", + ] + install_result = _probe( + run, + install_argv, + "Visual Studio installation", + deadline=deadline, + clock=clock, + ) + probe_cwd = Path.cwd().resolve() + installation = _require_path( + _first_path( + install_result, + "Visual Studio installation", + install_argv, + probe_cwd, + ), + "Visual Studio installation", + ) + msbuild_argv = [ + str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD, + "-find", r"MSBuild\**\Bin\MSBuild.exe", + ] + msbuild_result = _probe( + run, + msbuild_argv, + "MSBuild", + deadline=deadline, + clock=clock, + ) + msbuild = _require_path( + _first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd), + "MSBuild.exe", + ) + + if kind is not ProjectKind.CMAKE: + return Toolchain(installation=installation, msbuild=msbuild) + + bundled_cmake_dir = installation / VS_CMAKE_RELATIVE + cmake = _require_path( + Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe", + "cmake.exe", + ) + ctest = _require_path( + Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe", + "ctest.exe", + ) + return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest) diff --git a/scripts/test_agent_skill_references.py b/scripts/test_agent_skill_references.py deleted file mode 100644 index 8337ab6..0000000 --- a/scripts/test_agent_skill_references.py +++ /dev/null @@ -1,75 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENTS_ROOT = ROOT / ".codex" / "agents" - -AGENT_SKILL_REFERENCES = { - "coordinator-agent.toml": ( - "fesa-requirements-baseline", - "fesa-reference-models", - "fesa-release-readiness", - ), - "requirement-agent.toml": ("fesa-requirements-baseline",), - "research-agent.toml": ( - "fesa-research-evidence", - "fem-theory-query", - ), - "formulation-agent.toml": ( - "fesa-formulation-spec", - "fem-theory-query", - ), - "numerical-review-agent.toml": ( - "fesa-numerical-review", - "fem-theory-query", - ), - "io-definition-agent.toml": ( - "fesa-io-contract", - "fem-theory-query", - ), - "reference-model-agent.toml": ( - "fesa-reference-models", - "fem-theory-query", - ), - "implementation-planning-agent.toml": ( - "fesa-formulation-spec", - "fesa-reference-models", - "fesa-cpp-msvc-tdd", - "fem-theory-query", - ), - "implementation-agent.toml": ("fesa-cpp-msvc-tdd",), - "build-test-executor-agent.toml": ("fesa-cpp-msvc-tdd",), - "correction-agent.toml": ("fesa-cpp-msvc-tdd",), - "reference-verification-agent.toml": ( - "fesa-reference-comparison", - "fesa-io-contract", - ), - "physics-evaluation-agent.toml": ( - "fesa-physics-sanity", - "fem-theory-query", - ), - "release-agent.toml": ("fesa-release-readiness",), -} - - -class AgentSkillReferenceTests(unittest.TestCase): - def test_agents_reference_their_solver_skills(self): - for agent_file, skill_names in AGENT_SKILL_REFERENCES.items(): - with self.subTest(agent=agent_file): - text = (AGENTS_ROOT / agent_file).read_text(encoding="utf-8") - data = tomllib.loads(text) - instructions = data["developer_instructions"] - - self.assertIn("Skill references:", instructions) - for skill_name in skill_names: - self.assertIn(f"${skill_name}", instructions) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_build_test_executor_agent_config.py b/scripts/test_build_test_executor_agent_config.py deleted file mode 100644 index 781f39b..0000000 --- a/scripts/test_build_test_executor_agent_config.py +++ /dev/null @@ -1,86 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "build-test-executor-agent.toml" -BUILD_TEST_REPORTS_README = ROOT / "docs" / "build-test-reports" / "README.md" - - -class BuildTestExecutorAgentConfigTests(unittest.TestCase): - def test_build_test_executor_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "build-test-executor-agent") - self.assertIn("C++/MSVC/CMake/CTest validation", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_build_test_executor_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not edit source code.", - "Do not edit tests.", - "Do not edit CMake.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - ): - self.assertIn(required_text, instructions) - - def test_build_test_executor_agent_instructions_define_validation_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "python scripts/validate_workspace.py", - "HARNESS_VALIDATION_COMMANDS", - "msvc-debug", - "CMake/MSVC x64 Debug", - "ctest --test-dir", - "--output-on-failure", - "-C Debug", - ): - self.assertIn(required_text, instructions) - - def test_build_test_executor_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Execution Environment", - "Command Log Summary", - "Validation Results", - "Failure Classification", - "Failed Test Inventory", - "Handoff Recommendation", - "No-Change Assertion", - ): - self.assertIn(required_text, instructions) - - def test_build_test_report_guide_defines_template_and_status_values(self): - guide = BUILD_TEST_REPORTS_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/build-test-reports/-build-test.md", - "Execution Environment", - "Command Log Summary", - "Validation Results", - "Failure Classification", - "Failed Test Inventory", - "Handoff Recommendation", - "No-Change Assertion", - "pass-for-reference-verification", - "needs-correction", - "needs-environment-fix", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_coordinator_agent_config.py b/scripts/test_coordinator_agent_config.py deleted file mode 100644 index 5d1bd21..0000000 --- a/scripts/test_coordinator_agent_config.py +++ /dev/null @@ -1,102 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "coordinator-agent.toml" -COORDINATION_README = ROOT / "docs" / "coordination" / "README.md" - - -class CoordinatorAgentConfigTests(unittest.TestCase): - def test_coordinator_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "coordinator-agent") - self.assertIn("workflow state", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_coordinator_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not edit source code.", - "Do not edit tests.", - "Do not edit CMake.", - "Do not run build/test validation.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not automatically spawn subagents.", - "Do not approve release readiness independently.", - ): - self.assertIn(required_text, instructions) - - def test_coordinator_agent_instructions_define_workflow_and_status_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT", - "Requirement Agent", - "Research Agent", - "Formulation Agent", - "Numerical Review Agent", - "I/O Definition Agent", - "Reference Model Agent", - "Implementation Planning Agent", - "Implementation Agent", - "Build/Test Executor Agent", - "Correction Agent", - "Reference Verification Agent", - "Physics Evaluation Agent", - "Release Agent", - "ready-for-implementation", - "pass-for-reference-verification", - "pass-for-physics-evaluation", - "pass-for-release-agent", - "ready-for-release", - "completed", - ): - self.assertIn(required_text, instructions) - - def test_coordinator_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Gate Evidence Inventory", - "Decision Log", - "Next Agent Handoff", - "Traceability Snapshot", - "Risk and Blocker Register", - "Rework Loop Control", - "No-Change Assertion", - ): - self.assertIn(required_text, instructions) - - def test_coordination_guide_defines_template_and_status_values(self): - guide = COORDINATION_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/coordination/-coordination.md", - "Gate Evidence Inventory", - "Decision Log", - "Next Agent Handoff", - "Traceability Snapshot", - "Risk and Blocker Register", - "Rework Loop Control", - "No-Change Assertion", - "needs-user-decision", - "blocked", - "python scripts/validate_workspace.py", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_correction_agent_config.py b/scripts/test_correction_agent_config.py deleted file mode 100644 index 9248459..0000000 --- a/scripts/test_correction_agent_config.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "correction-agent.toml" -CORRECTIONS_README = ROOT / "docs" / "corrections" / "README.md" - - -class CorrectionAgentConfigTests(unittest.TestCase): - def test_correction_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "correction-agent") - self.assertIn("C++/MSVC/CMake/CTest fixes", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_correction_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not change requirements", - "Do not change formulations", - "Do not change I/O contracts", - "Do not change reference artifacts", - "Do not change tolerance policies", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - ): - self.assertIn(required_text, instructions) - - def test_correction_agent_instructions_define_triage_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "TRIAGE -> MINIMAL FIX -> VERIFY -> REPORT", - "configure", - "compile", - "link", - "test", - "reference-comparison", - "harness", - "environment", - "upstream-contract", - ): - self.assertIn(required_text, instructions) - - def test_correction_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Failure Triage", - "Root Cause Summary", - "Correction Scope", - "Verification Evidence", - "Traceability", - "Handoff Recommendation", - "Stop Condition", - ): - self.assertIn(required_text, instructions) - - def test_correction_report_guide_defines_template_and_status_values(self): - guide = CORRECTIONS_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/corrections/-correction.md", - "Failure Triage", - "Root Cause Summary", - "Correction Scope", - "Verification Evidence", - "Traceability", - "Handoff Recommendation", - "Stop Condition", - "corrected-for-build-test", - "needs-upstream-decision", - "python scripts/validate_workspace.py", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_execute.py b/scripts/test_execute.py deleted file mode 100644 index 5ac3768..0000000 --- a/scripts/test_execute.py +++ /dev/null @@ -1,286 +0,0 @@ -import importlib.util -import json -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path -from unittest.mock import patch - - -def load_execute(): - module_path = Path(__file__).resolve().parent / "execute.py" - spec = importlib.util.spec_from_file_location("execute", module_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def write_phase(root: Path, phase_dir: str = "0-mvp", phase_name: str = "0-mvp", steps=None): - phase_path = root / "phases" / phase_dir - phase_path.mkdir(parents=True) - if steps is None: - steps = [ - { - "step": 1, - "name": "Docs", - "status": "pending", - "summary": "", - "allowed_paths": ["docs/*.md"], - } - ] - (phase_path / "index.json").write_text( - json.dumps({"project": "FESA", "phase": phase_name, "steps": steps}, indent=2), - encoding="utf-8", - ) - (phase_path / "step1.md").write_text("# Step 1\n", encoding="utf-8") - return phase_path - - -def make_executor(execute, root: Path, phase_dir: str = "0-mvp"): - with patch.object(execute, "ROOT", root): - return execute.StepExecutor(phase_dir) - - -class ExecuteRunnerSafetyTests(unittest.TestCase): - def test_scaffold_loads_execute_module(self): - execute = load_execute() - - self.assertTrue(hasattr(execute, "StepExecutor")) - - def test_branch_name_uses_codex_prefix_and_sanitized_phase(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root, phase_name="linear truss/1d") - executor = make_executor(execute, root) - - self.assertEqual(executor._branch_name(), "codex/linear-truss-1d") - - def test_finalize_push_uses_codex_branch_name(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root, phase_name="0-mvp") - executor = make_executor(execute, root) - executor._auto_push = True - calls = [] - - def fake_git(*args): - calls.append(args) - if args == ("diff", "--cached", "--quiet"): - return subprocess.CompletedProcess(args, 0, "", "") - return subprocess.CompletedProcess(args, 0, "", "") - - with patch.object(executor, "_run_git", side_effect=fake_git): - with patch.object(executor, "_validate_before_commit", create=True): - with patch("builtins.print"): - executor._finalize() - - self.assertIn(("push", "-u", "origin", "codex/0-mvp"), calls) - - def test_finalize_stages_only_phase_indexes(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - (root / "phases" / "index.json").write_text('{"phases":[]}', encoding="utf-8") - executor = make_executor(execute, root) - calls = [] - - def fake_git(*args): - calls.append(args) - if args == ("diff", "--cached", "--quiet"): - return subprocess.CompletedProcess(args, 1, "", "") - return subprocess.CompletedProcess(args, 0, "", "") - - with patch.object(executor, "_run_git", side_effect=fake_git): - with patch.object(executor, "_validate_before_commit"): - with patch("builtins.print"): - executor._finalize() - - self.assertNotIn(("add", "-A"), calls) - self.assertIn(("add", "--", "phases/0-mvp/index.json", "phases/index.json"), calls) - - def test_assert_clean_worktree_exits_when_git_status_has_changes(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - - with patch.object( - executor, - "_run_git", - return_value=subprocess.CompletedProcess([], 0, " M AGENTS.md\n?? scratch.txt\n", ""), - ): - with patch("builtins.print"): - with self.assertRaises(SystemExit) as cm: - executor._assert_clean_worktree("before checkout") - - self.assertEqual(cm.exception.code, 1) - - def test_run_checks_clean_worktree_before_checkout(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - calls = [] - - def record(name): - def inner(*args, **kwargs): - calls.append(name) - return inner - - with patch.object(executor, "_assert_clean_worktree", side_effect=record("clean")): - with patch.object(executor, "_checkout_branch", side_effect=record("checkout")): - with patch.object(executor, "_print_header"): - with patch.object(executor, "_check_blockers"): - with patch.object(executor, "_load_guardrails", return_value=""): - with patch.object(executor, "_ensure_created_at"): - with patch.object(executor, "_execute_all_steps"): - with patch.object(executor, "_finalize"): - executor.run() - - self.assertLess(calls.index("clean"), calls.index("checkout")) - - def test_step_allowlist_accepts_exact_prefix_and_glob_paths(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - patterns = ["AGENTS.md", "docs/", "scripts/*.py"] - - self.assertTrue(executor._path_allowed("AGENTS.md", patterns)) - self.assertTrue(executor._path_allowed("docs/PRD.md", patterns)) - self.assertTrue(executor._path_allowed("scripts/execute.py", patterns)) - self.assertFalse(executor._path_allowed(".codex/hooks.json", patterns)) - - def test_step_without_allowed_paths_is_rejected_before_codex_invocation(self): - execute = load_execute() - steps = [{"step": 1, "name": "Unsafe", "status": "pending", "summary": ""}] - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root, steps=steps) - executor = make_executor(execute, root) - - with patch("builtins.print"): - with self.assertRaises(SystemExit) as cm: - executor._validate_step_allowlist(steps[0]) - - self.assertEqual(cm.exception.code, 1) - - def test_classify_step_changes_splits_allowed_housekeeping_and_disallowed_paths(self): - execute = load_execute() - step = { - "step": 1, - "name": "Docs", - "status": "completed", - "summary": "", - "allowed_paths": ["docs/*.md"], - } - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - changed = [ - "docs/PRD.md", - "phases/0-mvp/index.json", - "phases/0-mvp/step1-output.json", - "scripts/execute.py", - ] - - allowed, housekeeping, disallowed = executor._classify_step_changes(1, step, changed) - - self.assertEqual(allowed, ["docs/PRD.md"]) - self.assertEqual(housekeeping, ["phases/0-mvp/index.json", "phases/0-mvp/step1-output.json"]) - self.assertEqual(disallowed, ["scripts/execute.py"]) - - def test_commit_step_stages_only_explicit_allowed_and_housekeeping_paths(self): - execute = load_execute() - step = { - "step": 1, - "name": "Docs", - "status": "completed", - "summary": "", - "allowed_paths": ["docs/*.md"], - } - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - calls = [] - - def fake_git(*args): - calls.append(args) - if args in { - ("diff", "--quiet", "--cached", "--"), - ("diff", "--cached", "--quiet"), - }: - return subprocess.CompletedProcess(args, 1, "", "") - return subprocess.CompletedProcess(args, 0, "", "") - - with patch.object( - executor, - "_changed_paths", - return_value=[ - "docs/PRD.md", - "phases/0-mvp/index.json", - "phases/0-mvp/step1-output.json", - ], - ): - with patch.object(executor, "_run_git", side_effect=fake_git): - with patch.object(executor, "_validate_before_commit", create=True): - with patch("builtins.print"): - executor._commit_step(step, "Docs") - - self.assertNotIn(("add", "-A"), calls) - self.assertIn(("add", "--", "docs/PRD.md"), calls) - self.assertIn(("add", "--", "phases/0-mvp/index.json", "phases/0-mvp/step1-output.json"), calls) - - def test_validate_before_commit_runs_python_selftest_then_workspace_validation(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - commands = [] - - def fake_run(cmd, **kwargs): - commands.append(cmd) - return subprocess.CompletedProcess(cmd, 0, "ok", "") - - with patch.object(execute.subprocess, "run", side_effect=fake_run): - with patch("builtins.print"): - executor._validate_before_commit("feat(0-mvp): step 1") - - self.assertEqual( - commands, - [ - [sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"], - [sys.executable, "scripts/validate_workspace.py"], - ], - ) - - def test_validate_before_commit_exits_before_commit_when_validation_fails(self): - execute = load_execute() - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - write_phase(root) - executor = make_executor(execute, root) - - def fake_run(cmd, **kwargs): - return subprocess.CompletedProcess(cmd, 1, "bad", "failed") - - with patch.object(execute.subprocess, "run", side_effect=fake_run): - with patch("builtins.print"): - with self.assertRaises(SystemExit) as cm: - executor._validate_before_commit("feat(0-mvp): step 1") - - self.assertEqual(cm.exception.code, 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_fesa_solver_skills.py b/scripts/test_fesa_solver_skills.py deleted file mode 100644 index d9f435d..0000000 --- a/scripts/test_fesa_solver_skills.py +++ /dev/null @@ -1,344 +0,0 @@ -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SKILLS_ROOT = ROOT / ".codex" / "skills" -COMMON_SECTIONS = ( - "## Inputs", - "## Workflow", - "## Output Contract", - "## Boundaries", - "## Quality Gate", - "## Handoff", -) - -ACTIVE_CONTRACT_FILES = ( - ROOT / "AGENTS.md", - ROOT / "docs" / "ProjectInitialPlanNote.md", - ROOT / "docs" / "PRD.md", - ROOT / "docs" / "ARCHITECTURE.md", - ROOT / "docs" / "ADR.md", - ROOT / "docs" / "SOLVER_AGENT_DESIGN.md", - ROOT / "docs" / "SOLVER_SKILL_DESIGN.md", - ROOT / "docs" / "reference-models" / "README.md", - ROOT / "docs" / "reference-verifications" / "README.md", - ROOT / "docs" / "io-definitions" / "README.md", - ROOT / "docs" / "implementation-plans" / "README.md", - ROOT / "docs" / "physics-evaluations" / "README.md", - ROOT / "docs" / "requirements" / "README.md", - ROOT / ".codex" / "agents" / "reference-model-agent.toml", - ROOT / ".codex" / "agents" / "reference-verification-agent.toml", - ROOT / ".codex" / "agents" / "io-definition-agent.toml", - ROOT / ".codex" / "agents" / "implementation-planning-agent.toml", - ROOT / ".codex" / "agents" / "implementation-agent.toml", - ROOT / ".codex" / "agents" / "physics-evaluation-agent.toml", - ROOT / ".codex" / "agents" / "release-agent.toml", - ROOT / ".codex" / "agents" / "requirement-agent.toml", - ROOT / ".codex" / "agents" / "coordinator-agent.toml", - ROOT / ".codex" / "skills" / "fesa-reference-models" / "SKILL.md", - ROOT / ".codex" / "skills" / "fesa-reference-comparison" / "SKILL.md", - ROOT / ".codex" / "skills" / "fesa-io-contract" / "SKILL.md", - ROOT / ".codex" / "skills" / "fesa-physics-sanity" / "SKILL.md", - ROOT / ".codex" / "skills" / "fesa-cpp-msvc-tdd" / "SKILL.md", - ROOT / ".codex" / "skills" / "fesa-release-readiness" / "SKILL.md", - ROOT / ".codex" / "skills" / "fesa-requirements-baseline" / "SKILL.md", -) - -STALE_REFERENCE_CONTRACT_PHRASES = ( - "reference" ".h5", - "stored reference " "HDF5", - "reference " "HDF5 artifact", - "results.h5 and " "reference" ".h5", - "results.h5` and `reference" ".h5", - "derived from reference" ".h5", - "references/" "//", -) - - -SKILLS = { - "fesa-requirements-baseline": { - "description_terms": ( - "Use when", - "FESA solver", - "requirements", - "acceptance criteria", - "verification matrix", - ), - "body_terms": ( - "docs/requirements/.md", - "Requirement Verification Matrix", - "shall", - "FESA-REQ--###", - "Verification Quantities", - "Tolerance Policy", - "Reference Artifact Requirements", - "Do not implement C++ code.", - ), - }, - "fesa-research-evidence": { - "description_terms": ( - "Use when", - "FESA solver", - "research", - "FEM theory", - "benchmarks", - ), - "body_terms": ( - "docs/research/-research.md", - "Source Inventory", - "Source Reliability Tier", - "Candidate Benchmarks", - "Verification Relevance", - "Applicability Limits", - "Separate verified facts from inference.", - ), - }, - "fesa-formulation-spec": { - "description_terms": ( - "Use when", - "FESA FEM", - "formulation", - "element equations", - "output recovery", - ), - "body_terms": ( - "docs/formulations/-formulation.md", - "Strong Form", - "Weak or Variational Form", - "Discretization", - "Kinematics", - "Element Equations", - "Jacobian", - "Output Recovery", - "Do not design C++ APIs.", - ), - }, - "fesa-numerical-review": { - "description_terms": ( - "Use when", - "FESA FEM", - "numerical review", - "stability", - "implementation planning", - ), - "body_terms": ( - "docs/numerical-reviews/-review.md", - "pass-for-implementation-planning", - "rigid body modes", - "patch test", - "hourglass", - "locking", - "Jacobian", - "Do not edit formulations directly.", - ), - }, - "fesa-io-contract": { - "description_terms": ( - "Use when", - "FESA solver", - "Abaqus .inp", - "HDF5", - "CSV", - "I/O", - ), - "body_terms": ( - "docs/io-definitions/-io.md", - "Abaqus Input Scope", - "Internal Model Contract", - "Output HDF5 Schema", - "FESA HDF5 to Reference CSV Comparison Schema", - "results.h5", - "reference//", - "*NODE", - "*ELEMENT", - "*MATERIAL", - "*BOUNDARY", - "*STEP", - "Do not implement parsers.", - ), - }, - "fesa-reference-models": { - "description_terms": ( - "Use when", - "FESA", - "reference model", - "Abaqus input", - "CSV", - ), - "body_terms": ( - "docs/reference-models/-reference-models.md", - "reference//", - "model.inp", - "metadata.json", - "_displacements.csv", - "_reactions.csv", - "_internalforces.csv", - "_stresses.csv", - "Coverage Matrix", - "Do not generate or modify Abaqus reference CSV files.", - ), - }, - "fesa-cpp-msvc-tdd": { - "description_terms": ( - "Use when", - "FESA solver", - "C++", - "MSVC", - "TDD", - ), - "body_terms": ( - "docs/implementation-plans/-implementation-plan.md", - "RED -> GREEN -> VERIFY", - "python -m unittest discover -s scripts -p \"test_*.py\"", - "python scripts/validate_workspace.py", - "ctest", - "configure | compile | link | test | reference-comparison", - "Do not change requirements.", - ), - }, - "fesa-reference-comparison": { - "description_terms": ( - "Use when", - "FESA solver", - "HDF5", - "reference CSV", - "tolerance", - "comparison", - ), - "body_terms": ( - "docs/reference-verifications/-reference-verification.md", - "ARTIFACT CHECK -> COMPARE -> CLASSIFY -> REPORT", - "results.h5", - "Abaqus reference CSV", - "reference//", - "_displacements.csv", - "_reactions.csv", - "_internalforces.csv", - "_stresses.csv", - "max absolute error", - "max relative error", - "RMS error", - "missing rows", - "extra rows", - "pass-for-physics-evaluation", - "Do not change tolerance policies.", - ), - }, - "fesa-physics-sanity": { - "description_terms": ( - "Use when", - "FESA solver", - "physical plausibility", - "equilibrium", - "physics", - ), - "body_terms": ( - "docs/physics-evaluations/-physics-evaluation.md", - "global equilibrium", - "reaction consistency", - "displacement direction", - "symmetry", - "element force balance", - "model coverage", - "pass-for-release-agent", - "Do not approve release readiness.", - ), - }, - "fesa-release-readiness": { - "description_terms": ( - "Use when", - "FESA solver", - "release readiness", - "release notes", - "known limitations", - ), - "body_terms": ( - "docs/releases/-release.md", - "GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT", - "ready-for-release", - "Known Limitations", - "Release Notes Draft", - "pass-for-reference-verification", - "pass-for-physics-evaluation", - "pass-for-release-agent", - "Do not publish, deploy, package, tag, commit, or externally release anything unless the user explicitly asks.", - ), - }, -} - - -def read_skill(skill_name): - return (SKILLS_ROOT / skill_name / "SKILL.md").read_text(encoding="utf-8") - - -def parse_frontmatter(text): - lines = text.splitlines() - if not lines or lines[0] != "---": - raise AssertionError("SKILL.md must start with YAML frontmatter") - - fields = {} - for line in lines[1:]: - if line == "---": - return fields - key, sep, value = line.partition(":") - if not sep: - raise AssertionError(f"Invalid frontmatter line: {line}") - fields[key.strip()] = value.strip() - - raise AssertionError("SKILL.md frontmatter must be closed") - - -class FesaSolverSkillTests(unittest.TestCase): - def test_all_solver_skill_files_exist_with_required_frontmatter(self): - for skill_name, spec in SKILLS.items(): - with self.subTest(skill=skill_name): - skill_path = SKILLS_ROOT / skill_name / "SKILL.md" - self.assertTrue(skill_path.exists(), f"{skill_name} SKILL.md is missing") - - fields = parse_frontmatter(read_skill(skill_name)) - - self.assertEqual(set(fields), {"name", "description"}) - self.assertEqual(fields["name"], skill_name) - for term in spec["description_terms"]: - self.assertIn(term, fields["description"]) - - def test_all_solver_skills_define_common_contract_sections(self): - for skill_name in SKILLS: - with self.subTest(skill=skill_name): - body = read_skill(skill_name) - for section in COMMON_SECTIONS: - self.assertIn(section, body) - self.assertIn("AGENTS.md", body) - self.assertIn("docs/SOLVER_AGENT_DESIGN.md", body) - self.assertNotIn("docs/SOLVER_SKILL_DESIGN.md", body) - - def test_solver_skills_define_skill_specific_contracts(self): - for skill_name, spec in SKILLS.items(): - with self.subTest(skill=skill_name): - body = read_skill(skill_name) - for term in spec["body_terms"]: - self.assertIn(term, body) - - def test_solver_skills_have_openai_ui_metadata(self): - for skill_name in SKILLS: - with self.subTest(skill=skill_name): - metadata = SKILLS_ROOT / skill_name / "agents" / "openai.yaml" - self.assertTrue(metadata.exists(), f"{skill_name} openai.yaml is missing") - text = metadata.read_text(encoding="utf-8") - self.assertIn("interface:", text) - self.assertIn("display_name:", text) - self.assertIn("short_description:", text) - self.assertIn("default_prompt:", text) - self.assertIn(f"${skill_name}", text) - - def test_active_contracts_do_not_require_reference_hdf5(self): - for path in ACTIVE_CONTRACT_FILES: - with self.subTest(path=str(path.relative_to(ROOT))): - text = path.read_text(encoding="utf-8") - for stale_phrase in STALE_REFERENCE_CONTRACT_PHRASES: - self.assertNotIn(stale_phrase, text) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_formulation_agent_config.py b/scripts/test_formulation_agent_config.py deleted file mode 100644 index 05a75ae..0000000 --- a/scripts/test_formulation_agent_config.py +++ /dev/null @@ -1,88 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "formulation-agent.toml" -FORMULATIONS_README = ROOT / "docs" / "formulations" / "README.md" - - -class FormulationAgentConfigTests(unittest.TestCase): - def test_formulation_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "formulation-agent") - self.assertIn("FEM formulation", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_formulation_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not design C++ APIs", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - "docs/SOLVER_AGENT_DESIGN.md", - "docs/requirements/.md", - "docs/research/-research.md", - ): - self.assertIn(required_text, instructions) - - def test_formulation_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Strong Form and Boundary Conditions", - "Weak or Variational Form", - "Discretization", - "Kinematics", - "Constitutive Contract", - "Element Equations", - "Mapping and Numerical Integration", - "Output Recovery", - "Numerical Risks", - "Downstream Handoff", - ): - self.assertIn(required_text, instructions) - - def test_formulation_agent_instructions_define_numerical_risk_policy(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "rigid body modes", - "patch test", - "hourglass", - "locking", - "Jacobian", - ): - self.assertIn(required_text, instructions) - - def test_formulation_document_guide_defines_output_contract(self): - guide = FORMULATIONS_README.read_text(encoding="utf-8") - - for required_text in ( - "Strong Form and Boundary Conditions", - "Weak or Variational Form", - "Discretization", - "Kinematics", - "Constitutive Contract", - "Element Equations", - "Mapping and Numerical Integration", - "Output Recovery", - "Numerical Risks", - "Downstream Handoff", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_implementation_agent_config.py b/scripts/test_implementation_agent_config.py deleted file mode 100644 index a60bdd3..0000000 --- a/scripts/test_implementation_agent_config.py +++ /dev/null @@ -1,81 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "implementation-agent.toml" - - -class ImplementationAgentConfigTests(unittest.TestCase): - def test_implementation_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "implementation-agent") - self.assertIn("C++17/MSVC", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_implementation_agent_instructions_define_tdd_execution_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Write tests first", - "verify failure", - "minimum code", - "C++17", - "MSVC", - "CMake", - "CTest", - "RED -> GREEN -> VERIFY", - ): - self.assertIn(required_text, instructions) - - def test_implementation_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - "Do not change requirements", - "Do not change formulations", - "Do not change I/O contracts", - "Do not change reference artifacts", - "Do not produce the final reference verification report.", - ): - self.assertIn(required_text, instructions) - - def test_implementation_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Test Evidence", - "Code Changes", - "Validation Evidence", - "Traceability", - "Downstream Handoff", - "Build/Test Executor Agent", - "Correction Agent", - "Reference Verification Agent", - ): - self.assertIn(required_text, instructions) - - def test_implementation_agent_instructions_define_validation_commands(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "python -m unittest discover -s scripts -p \"test_*.py\"", - "python scripts/validate_workspace.py", - "ctest -C Debug", - ): - self.assertIn(required_text, instructions) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_implementation_planning_agent_config.py b/scripts/test_implementation_planning_agent_config.py deleted file mode 100644 index 1e313de..0000000 --- a/scripts/test_implementation_planning_agent_config.py +++ /dev/null @@ -1,87 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "implementation-planning-agent.toml" -IMPLEMENTATION_PLANS_README = ROOT / "docs" / "implementation-plans" / "README.md" - - -class ImplementationPlanningAgentConfigTests(unittest.TestCase): - def test_implementation_planning_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "implementation-planning-agent") - self.assertIn("TDD-first C++/MSVC implementation plans", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_implementation_planning_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not write tests.", - "Do not edit CMake.", - "Do not run CMake/CTest.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not compare solver results.", - "Do not approve release readiness.", - ): - self.assertIn(required_text, instructions) - - def test_implementation_planning_agent_instructions_define_tdd_msvc_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "C++17", - "MSVC", - "CMake", - "CTest", - "TDD", - "failing unit tests first", - "reference comparison tests", - ): - self.assertIn(required_text, instructions) - - def test_implementation_planning_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Readiness Check", - "Work Breakdown", - "TDD Test Plan", - "CMake/CTest Plan", - "Acceptance Traceability Matrix", - "Validation Commands", - "Downstream Handoff", - ): - self.assertIn(required_text, instructions) - - def test_implementation_planning_document_guide_defines_output_contract(self): - guide = IMPLEMENTATION_PLANS_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/implementation-plans/-implementation-plan.md", - "Readiness Check", - "Work Breakdown", - "TDD Test Plan", - "CMake/CTest Plan", - "Acceptance Traceability Matrix", - "Validation Commands", - "Downstream Handoff", - "python scripts/validate_workspace.py", - "ctest -C Debug", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_io_definition_agent_config.py b/scripts/test_io_definition_agent_config.py deleted file mode 100644 index 9122b1c..0000000 --- a/scripts/test_io_definition_agent_config.py +++ /dev/null @@ -1,101 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "io-definition-agent.toml" -IO_DEFINITIONS_README = ROOT / "docs" / "io-definitions" / "README.md" - - -class IoDefinitionAgentConfigTests(unittest.TestCase): - def test_io_definition_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "io-definition-agent") - self.assertIn("Abaqus input-file subsets", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_io_definition_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement parsers.", - "Do not design C++ APIs", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - "Do not claim full Abaqus compatibility", - ): - self.assertIn(required_text, instructions) - - def test_io_definition_agent_instructions_define_abaqus_input_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "FESA solver input files are Abaqus input files.", - "Abaqus input files use keyword lines, data lines, and comment lines.", - "Model data and history data", - "supported Abaqus keyword subset", - "HDF5 result schema", - "reference CSV comparison row schema", - ): - self.assertIn(required_text, instructions) - - def test_io_definition_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Abaqus Input Scope", - "Syntax Policy", - "Model Data Mapping", - "History Data Mapping", - "Internal Model Contract", - "Output HDF5 Schema", - "FESA HDF5 to Reference CSV Comparison Schema", - "Validation Rules", - "Downstream Handoff", - ): - self.assertIn(required_text, instructions) - - def test_io_definition_agent_instructions_define_keyword_policy(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "*NODE", - "*ELEMENT", - "*MATERIAL", - "*BOUNDARY", - "*STEP", - "*OUTPUT", - "*NODE OUTPUT", - "*ELEMENT OUTPUT", - ): - self.assertIn(required_text, instructions) - - def test_io_definition_document_guide_defines_output_contract(self): - guide = IO_DEFINITIONS_README.read_text(encoding="utf-8") - - for required_text in ( - "Abaqus Input Scope", - "Syntax Policy", - "Model Data Mapping", - "History Data Mapping", - "Internal Model Contract", - "Output HDF5 Schema", - "FESA HDF5 to Reference CSV Comparison Schema", - "Validation Rules", - "Downstream Handoff", - "FESA 솔버의 입력 파일은 Abaqus input file이다.", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_numerical_review_agent_config.py b/scripts/test_numerical_review_agent_config.py deleted file mode 100644 index 8144a78..0000000 --- a/scripts/test_numerical_review_agent_config.py +++ /dev/null @@ -1,86 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "numerical-review-agent.toml" -NUMERICAL_REVIEWS_README = ROOT / "docs" / "numerical-reviews" / "README.md" - - -class NumericalReviewAgentConfigTests(unittest.TestCase): - def test_numerical_review_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "numerical-review-agent") - self.assertIn("numerical correctness", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_numerical_review_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not edit formulations directly.", - "Do not design C++ APIs", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - "docs/SOLVER_AGENT_DESIGN.md", - "docs/formulations/-formulation.md", - ): - self.assertIn(required_text, instructions) - - def test_numerical_review_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Review Verdict", - "Critical Findings", - "Numerical Risk Assessment", - "Consistency Checks", - "Verification Readiness", - "Required Revisions", - "Downstream Handoff", - ): - self.assertIn(required_text, instructions) - - def test_numerical_review_agent_instructions_define_risk_policy(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "rigid body modes", - "patch test", - "symmetry", - "positive definiteness", - "hourglass", - "locking", - "singular Jacobian", - "conditioning", - ): - self.assertIn(required_text, instructions) - - def test_numerical_review_document_guide_defines_output_contract(self): - guide = NUMERICAL_REVIEWS_README.read_text(encoding="utf-8") - - for required_text in ( - "Review Verdict", - "Critical Findings", - "Numerical Risk Assessment", - "Consistency Checks", - "Verification Readiness", - "Required Revisions", - "Downstream Handoff", - "pass-for-implementation-planning", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_physics_evaluation_agent_config.py b/scripts/test_physics_evaluation_agent_config.py deleted file mode 100644 index c4e6c87..0000000 --- a/scripts/test_physics_evaluation_agent_config.py +++ /dev/null @@ -1,89 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "physics-evaluation-agent.toml" -PHYSICS_EVALUATIONS_README = ROOT / "docs" / "physics-evaluations" / "README.md" - - -class PhysicsEvaluationAgentConfigTests(unittest.TestCase): - def test_physics_evaluation_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "physics-evaluation-agent") - self.assertIn("physical plausibility", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_physics_evaluation_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not edit source code.", - "Do not edit tests.", - "Do not edit CMake.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not change tolerances.", - "Do not approve release readiness.", - ): - self.assertIn(required_text, instructions) - - def test_physics_evaluation_agent_instructions_define_physics_checks(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "global equilibrium", - "reaction consistency", - "displacement direction", - "symmetry", - "element force balance", - "stress/strain", - "rigid body mode", - "energy/residual", - ): - self.assertIn(required_text, instructions) - - def test_physics_evaluation_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "HDF5", - "Abaqus reference CSV files", - "Input Evidence", - "Physics Checks", - "Failure Classification", - "Evaluation Verdict", - "Handoff Recommendation", - "No-Change Assertion", - ): - self.assertIn(required_text, instructions) - - def test_physics_evaluation_report_guide_defines_template_and_status_values(self): - guide = PHYSICS_EVALUATIONS_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/physics-evaluations/-physics-evaluation.md", - "Input Evidence", - "Physics Checks", - "Failure Classification", - "Evaluation Verdict", - "Handoff Recommendation", - "No-Change Assertion", - "pass-for-release-agent", - "needs-correction", - "needs-reference-model", - "needs-upstream-decision", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_pre_commit_checks.py b/scripts/test_pre_commit_checks.py deleted file mode 100644 index 8553716..0000000 --- a/scripts/test_pre_commit_checks.py +++ /dev/null @@ -1,41 +0,0 @@ -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() diff --git a/scripts/test_reference_model_agent_config.py b/scripts/test_reference_model_agent_config.py deleted file mode 100644 index 4906b05..0000000 --- a/scripts/test_reference_model_agent_config.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "reference-model-agent.toml" -REFERENCE_MODELS_README = ROOT / "docs" / "reference-models" / "README.md" - - -class ReferenceModelAgentConfigTests(unittest.TestCase): - def test_reference_model_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "reference-model-agent") - self.assertIn("reference model packages", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_reference_model_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not implement parsers.", - "Do not design C++ APIs", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not compare solver results.", - "Do not approve release readiness.", - ): - self.assertIn(required_text, instructions) - - def test_reference_model_agent_instructions_define_reference_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "FESA reference models use Abaqus input files.", - "reference//", - "model.inp", - "metadata.json", - "_displacements.csv", - "_reactions.csv", - "_internalforces.csv", - "_stresses.csv", - ): - self.assertIn(required_text, instructions) - - self.assertNotIn("reference" ".h5", instructions) - self.assertNotIn("references/" "//", instructions) - - def test_reference_model_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Reference Strategy", - "Model Inventory", - "Abaqus Input Requirements", - "Artifact Bundle Contract", - "Metadata JSON Contract", - "Abaqus Reference CSV Requirements", - "Coverage Matrix", - "Downstream Handoff", - ): - self.assertIn(required_text, instructions) - - def test_reference_model_document_guide_defines_output_contract(self): - guide = REFERENCE_MODELS_README.read_text(encoding="utf-8") - - for required_text in ( - "Reference Strategy", - "Model Inventory", - "Abaqus Input Requirements", - "Artifact Bundle Contract", - "Metadata JSON Contract", - "Abaqus Reference CSV Requirements", - "Coverage Matrix", - "Downstream Handoff", - "reference//", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_reference_verification_agent_config.py b/scripts/test_reference_verification_agent_config.py deleted file mode 100644 index 0c24a5b..0000000 --- a/scripts/test_reference_verification_agent_config.py +++ /dev/null @@ -1,91 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "reference-verification-agent.toml" -REFERENCE_VERIFICATIONS_README = ROOT / "docs" / "reference-verifications" / "README.md" - - -class ReferenceVerificationAgentConfigTests(unittest.TestCase): - def test_reference_verification_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "reference-verification-agent") - self.assertIn("HDF5", data["description"]) - self.assertIn("Abaqus reference CSV", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_reference_verification_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not edit source code.", - "Do not edit tests.", - "Do not edit CMake.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not approve release readiness.", - "Do not change tolerance policies.", - ): - self.assertIn(required_text, instructions) - - def test_reference_verification_agent_instructions_define_artifact_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "results.h5", - "Abaqus reference CSV", - "reference//", - "_displacements.csv", - "_reactions.csv", - "_internalforces.csv", - "_stresses.csv", - "metadata.json", - ): - self.assertIn(required_text, instructions) - - self.assertNotIn("reference" ".h5", instructions) - self.assertNotIn("stored reference " "HDF5", instructions) - - def test_reference_verification_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Artifact Inventory", - "Comparison Contract", - "Quantity Results", - "Failure Classification", - "Handoff Recommendation", - "No-Change Assertion", - ): - self.assertIn(required_text, instructions) - - def test_reference_verification_report_guide_defines_template_and_status_values(self): - guide = REFERENCE_VERIFICATIONS_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/reference-verifications/-reference-verification.md", - "Artifact Inventory", - "Comparison Contract", - "Quantity Results", - "Failure Classification", - "Handoff Recommendation", - "No-Change Assertion", - "pass-for-physics-evaluation", - "needs-correction", - "needs-reference-artifacts", - "needs-upstream-decision", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_release_agent_config.py b/scripts/test_release_agent_config.py deleted file mode 100644 index dcc84ab..0000000 --- a/scripts/test_release_agent_config.py +++ /dev/null @@ -1,92 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "release-agent.toml" -RELEASES_README = ROOT / "docs" / "releases" / "README.md" - - -class ReleaseAgentConfigTests(unittest.TestCase): - def test_release_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "release-agent") - self.assertIn("release readiness", data["description"]) - self.assertEqual(data["sandbox_mode"], "workspace-write") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_release_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not edit source code.", - "Do not edit tests.", - "Do not edit CMake.", - "Do not change requirements", - "Do not change formulations", - "Do not change I/O contracts", - "Do not change reference artifacts", - "Do not change tolerance policies", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Do not override failed or missing upstream gates.", - ): - self.assertIn(required_text, instructions) - - def test_release_agent_instructions_define_gate_and_status_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT", - "pass-for-release-agent", - "pass-for-physics-evaluation", - "pass-for-reference-verification", - "ready-for-release", - "known limitations", - ): - self.assertIn(required_text, instructions) - - def test_release_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Gate Evidence Inventory", - "Acceptance Traceability", - "Validation Evidence", - "Known Limitations", - "Release Notes Draft", - "Release Verdict", - "No-Change Assertion", - ): - self.assertIn(required_text, instructions) - - def test_release_report_guide_defines_template_and_status_values(self): - guide = RELEASES_README.read_text(encoding="utf-8") - - for required_text in ( - "docs/releases/-release.md", - "Gate Evidence Inventory", - "Acceptance Traceability", - "Validation Evidence", - "Known Limitations", - "Release Notes Draft", - "Release Verdict", - "No-Change Assertion", - "ready-for-release", - "needs-documentation", - "needs-upstream-decision", - "python scripts/validate_workspace.py", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_requirement_agent_config.py b/scripts/test_requirement_agent_config.py deleted file mode 100644 index 0f40029..0000000 --- a/scripts/test_requirement_agent_config.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "requirement-agent.toml" -REQUIREMENTS_README = ROOT / "docs" / "requirements" / "README.md" - - -class RequirementAgentConfigTests(unittest.TestCase): - def test_requirement_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "requirement-agent") - self.assertIn("verifiable requirements", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_requirement_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not write finite element formulations.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "Requirement Verification Matrix", - "docs/SOLVER_AGENT_DESIGN.md", - "reference//", - ): - self.assertIn(required_text, instructions) - - def test_requirement_document_guide_defines_output_contract(self): - guide = REQUIREMENTS_README.read_text(encoding="utf-8") - - for required_text in ( - "feature_id", - "Verification Quantities", - "Tolerance Policy", - "Reference Artifact Requirements", - "Requirement Verification Matrix", - "Downstream Handoff", - "FESA-REQ--001", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_research_agent_config.py b/scripts/test_research_agent_config.py deleted file mode 100644 index cbaafa5..0000000 --- a/scripts/test_research_agent_config.py +++ /dev/null @@ -1,79 +0,0 @@ -import unittest -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - import tomli as tomllib - - -ROOT = Path(__file__).resolve().parents[1] -AGENT_PATH = ROOT / ".codex" / "agents" / "research-agent.toml" -RESEARCH_README = ROOT / "docs" / "research" / "README.md" - - -class ResearchAgentConfigTests(unittest.TestCase): - def test_research_agent_toml_has_required_codex_fields(self): - data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8")) - - self.assertEqual(data["name"], "research-agent") - self.assertIn("FEM theory", data["description"]) - self.assertEqual(data["sandbox_mode"], "read-only") - self.assertEqual(data["model_reasoning_effort"], "extra high") - self.assertIn("developer_instructions", data) - - def test_research_agent_instructions_enforce_boundaries(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Do not implement code.", - "Do not finalize FEM formulations.", - "Do not run Abaqus, Nastran, or any reference solver.", - "Do not generate or modify Abaqus reference CSV files.", - "docs/SOLVER_AGENT_DESIGN.md", - "docs/requirements/.md", - "Separate verified facts from inference.", - ): - self.assertIn(required_text, instructions) - - def test_research_agent_instructions_define_output_contract(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "Source Inventory", - "Candidate Benchmarks", - "Verification Relevance", - "Applicability Limits", - "Downstream Handoff", - ): - self.assertIn(required_text, instructions) - - def test_research_agent_instructions_define_source_policy(self): - instructions = AGENT_PATH.read_text(encoding="utf-8") - - for required_text in ( - "ASME V&V 10", - "Abaqus Verification Guide", - "Abaqus Benchmarks Guide", - "NAFEMS benchmarks", - "NASA FEMCI", - "MMS and MES papers", - ): - self.assertIn(required_text, instructions) - - def test_research_document_guide_defines_output_contract(self): - guide = RESEARCH_README.read_text(encoding="utf-8") - - for required_text in ( - "Source Reliability Tier", - "Source Inventory", - "Candidate Benchmarks", - "Verification Relevance", - "Applicability Limits", - "Downstream Handoff", - ): - self.assertIn(required_text, guide) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_tdd_guard.py b/scripts/test_tdd_guard.py deleted file mode 100644 index 67436df..0000000 --- a/scripts/test_tdd_guard.py +++ /dev/null @@ -1,61 +0,0 @@ -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() diff --git a/scripts/test_validate_workspace.py b/scripts/test_validate_workspace.py deleted file mode 100644 index 71ba75a..0000000 --- a/scripts/test_validate_workspace.py +++ /dev/null @@ -1,94 +0,0 @@ -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() diff --git a/scripts/validate_workspace.py b/scripts/validate_workspace.py deleted file mode 100644 index 45b4530..0000000 --- a/scripts/validate_workspace.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/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())