modify harness framework
This commit is contained in:
@@ -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": "<task-name>",
|
||||||
|
"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`을 삭제한
|
||||||
|
뒤 재실행
|
||||||
@@ -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이
|
||||||
|
없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다.
|
||||||
@@ -10,7 +10,7 @@ Mission:
|
|||||||
- Run build and test validation only after Implementation Agent work.
|
- Run build and test validation only after Implementation Agent work.
|
||||||
- Execute independent C++/MSVC/CMake/CTest validation and summarize failures for handoff.
|
- 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.
|
- 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:
|
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.
|
- 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.
|
2. Implementation Agent report.
|
||||||
3. docs/implementation-plans/<feature-id>-implementation-plan.md.
|
3. docs/implementation-plans/<feature-id>-implementation-plan.md.
|
||||||
4. AGENTS.md and docs/SOLVER_AGENT_DESIGN.md.
|
4. AGENTS.md and docs/SOLVER_AGENT_DESIGN.md.
|
||||||
5. scripts/validate_workspace.py.
|
5. `.harness/config.json` when present.
|
||||||
6. CMakePresets.json, CMakeLists.txt, CMake files, and CTest metadata when present.
|
6. CMakePresets.json, CMakeLists.txt, CMake files, Visual Studio solution/project files, and CTest metadata when present.
|
||||||
7. Related docs/reference-models/<feature-id>-reference-models.md when present.
|
7. Related docs/reference-models/<feature-id>-reference-models.md when present.
|
||||||
8. Stored reference artifacts when present, read-only.
|
8. Stored reference artifacts when present, read-only.
|
||||||
|
|
||||||
Execution contract:
|
Execution contract:
|
||||||
- Default validation is python scripts/validate_workspace.py.
|
- Resolve the validation path from `.harness/config.json` first, then Harness project auto detection.
|
||||||
- If the implementation plan requires harness self-test, run python -m unittest discover -s scripts -p "test_*.py" first.
|
- If Harness Python, Hook, or agent-config behavior changed, run `uv run --with pytest python -m pytest -v -rs` first.
|
||||||
- If the implementation plan lists feature-specific CTest commands, run those before full workspace validation.
|
- Configure and build before running feature-specific and full tests.
|
||||||
- Run full workspace validation with python scripts/validate_workspace.py last.
|
- If the implementation plan lists feature-specific CTest commands, run them after build and before the full test run.
|
||||||
- scripts/validate_workspace.py resolves HARNESS_VALIDATION_COMMANDS, CMakePresets.json msvc-debug, or CMake/MSVC x64 Debug commands.
|
- For a non-preset CMake project, run:
|
||||||
- The default CMake/MSVC x64 Debug commands are:
|
1. cmake -S . -B .harness/build -A x64
|
||||||
1. cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
|
2. cmake --build .harness/build --config Debug
|
||||||
2. cmake --build build/msvc-debug --config Debug
|
3. ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure when specified
|
||||||
3. ctest --test-dir build/msvc-debug --output-on-failure -C Debug
|
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.
|
- 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.
|
- Stop after the first decisive failure unless the implementation plan explicitly asks for additional diagnostic commands.
|
||||||
|
|
||||||
Failure classification:
|
Failure classification:
|
||||||
@@ -57,13 +60,13 @@ Failure classification:
|
|||||||
- link: link step failed.
|
- link: link step failed.
|
||||||
- test: CTest or unit/integration tests failed.
|
- test: CTest or unit/integration tests failed.
|
||||||
- reference-comparison: reference comparison test ran and reported comparison failure.
|
- 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.
|
- 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.
|
- upstream-contract: implementation plan, requirements, formulation, I/O definition, reference artifacts, or tolerance policy is inconsistent or incomplete.
|
||||||
|
|
||||||
Required Build/Test Report sections:
|
Required Build/Test Report sections:
|
||||||
1. Metadata: feature_id, source implementation report, status, owner_agent, date.
|
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.
|
3. Command Log Summary: command, exit code, duration, stdout/stderr tail.
|
||||||
4. Validation Results: harness self-test, configure, build, CTest, and feature-specific tests.
|
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.
|
5. Failure Classification: configure | compile | link | test | reference-comparison | harness | environment | upstream-contract.
|
||||||
|
|||||||
@@ -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: 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.
|
- 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: rerun the targeted command that reproduced the failure first.
|
||||||
- VERIFY: run python scripts/validate_workspace.py after the targeted command.
|
- VERIFY: run the full MSVC build/test commands resolved from `.harness/config.json` or Harness auto detection 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 `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 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 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.
|
- 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.
|
- link: linker, symbol resolution, library registration, or target dependency failed.
|
||||||
- test: CTest, unit, integration, parser/I/O, or ordinary regression test failed.
|
- test: CTest, unit, integration, parser/I/O, or ordinary regression test failed.
|
||||||
- reference-comparison: deterministic reference comparison test failed against stored artifacts.
|
- 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.
|
- 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.
|
- 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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
8. Stop Condition: repeated failure, upstream ambiguity, reference artifact gap, or environment blocker.
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ Execution contract:
|
|||||||
- RED: write the planned C++ unit, integration, parser/I/O, or reference-comparison test first.
|
- 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.
|
- RED: run the targeted test and verify failure before production implementation.
|
||||||
- GREEN: implement the minimum code needed for the planned task and acceptance criterion.
|
- 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.
|
- 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.
|
- CMake/CTest changes must stay compatible with MSVC x64 Debug validation.
|
||||||
- Abaqus reference CSV files are read-only verification inputs.
|
- 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.
|
2. Implemented Scope: completed task ids, skipped task ids, and reason.
|
||||||
3. Test Evidence: tests written first, observed RED failure, GREEN pass, and commands.
|
3. Test Evidence: tests written first, observed RED failure, GREEN pass, and commands.
|
||||||
4. Code Changes: source, header, test, and CMake/CTest change summary.
|
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.
|
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.
|
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.
|
8. Downstream Handoff: Build/Test Executor Agent, Correction Agent, and Reference Verification Agent.
|
||||||
|
|
||||||
Validation commands:
|
Validation commands:
|
||||||
- python -m unittest discover -s scripts -p "test_*.py"
|
- cmake -S . -B .harness/build -A x64
|
||||||
- python scripts/validate_workspace.py
|
- cmake --build .harness/build --config Debug
|
||||||
- ctest -C Debug -R <feature-or-label>
|
- ctest --test-dir .harness/build -C Debug -R <feature-or-label> --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:
|
Status rules:
|
||||||
- in-progress: implementation is underway.
|
- in-progress: implementation is underway.
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ Required Implementation Plan sections:
|
|||||||
3. Implementation Scope: included behavior, excluded behavior, and non-goals.
|
3. Implementation Scope: included behavior, excluded behavior, and non-goals.
|
||||||
4. Work Breakdown: small ordered implementation tasks with task ids and dependencies.
|
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.
|
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.
|
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/<model-id>/, and FESA HDF5-to-reference-CSV comparison flow.
|
8. Data Flow Contract: Abaqus .inp input, internal model, solver results.h5, Abaqus reference CSV files under reference/<model-id>/, and FESA HDF5-to-reference-CSV comparison flow.
|
||||||
9. Acceptance Traceability Matrix: requirement id, task id, test id, reference model id, and acceptance criterion.
|
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.
|
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.
|
12. Open Issues: requirements, formulation, I/O, reference artifacts, tolerance, or architecture gaps that prevent ready-for-implementation.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
8. Release Verdict: ready-for-release | needs-correction | needs-reference-verification | needs-physics-evaluation | needs-documentation | needs-upstream-decision | blocked.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#:schema https://developers.openai.com/codex/config-schema.json
|
#:schema https://developers.openai.com/codex/config-schema.json
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
codex_hooks = true
|
hooks = true
|
||||||
|
|||||||
+13
-9
@@ -1,25 +1,29 @@
|
|||||||
{
|
{
|
||||||
|
"description": "Harness TDD, command safety, and MSVC C/C++ validation hooks.",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PreToolUse": [
|
"PreToolUse": [
|
||||||
{
|
{
|
||||||
"matcher": "^Bash$",
|
"matcher": "Bash|shell_command|PowerShell|apply_patch|Edit|MultiEdit|Write",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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__')\"",
|
"command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"",
|
||||||
"timeout": 600,
|
"commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"",
|
||||||
"statusMessage": "Running pre-commit checks"
|
"timeout": 30,
|
||||||
|
"statusMessage": "Checking Harness policies"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
{
|
{
|
||||||
"matcher": "^(apply_patch|Edit|Write)$",
|
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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__')\"",
|
"command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"",
|
||||||
"timeout": 30,
|
"commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"",
|
||||||
"statusMessage": "Checking TDD guard"
|
"timeout": 1800,
|
||||||
|
"statusMessage": "Running MSVC build and tests"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -26,10 +26,12 @@ Read these first:
|
|||||||
3. RED: write the planned unit, integration, parser/I/O, or reference-comparison test 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.
|
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.
|
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.
|
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`.
|
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. Fix implementation-owned failures only and keep changes traceable to the implementation plan.
|
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
|
## Output Contract
|
||||||
|
|
||||||
@@ -43,17 +45,19 @@ Produce one of these, depending on role:
|
|||||||
Required validation commands:
|
Required validation commands:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
cmake -S . -B .harness/build -A x64
|
||||||
python scripts/validate_workspace.py
|
cmake --build .harness/build --config Debug
|
||||||
ctest -C Debug -R <feature-or-label>
|
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --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
|
```powershell
|
||||||
cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
|
uv run --with pytest python -m pytest -v -rs
|
||||||
cmake --build build/msvc-debug --config Debug
|
|
||||||
ctest --test-dir build/msvc-debug --output-on-failure -C Debug
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Boundaries
|
## 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.
|
- 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.
|
- Each test has a clear RED condition, GREEN condition, linked task, and command.
|
||||||
- CMake/CTest plans remain compatible with MSVC x64 Debug validation.
|
- 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.
|
- Build/test reports record command, exit code, duration, stdout/stderr tail, and failure classification.
|
||||||
- Correction attempts stop when repeated failure indicates upstream contract ambiguity.
|
- Correction attempts stop when repeated failure indicates upstream contract ambiguity.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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."
|
|
||||||
@@ -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": "<task-name>",
|
|
||||||
"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.
|
|
||||||
@@ -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."
|
|
||||||
@@ -18,6 +18,10 @@ Testing/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
# local Harness configuration and build outputs
|
||||||
|
.harness/config.json
|
||||||
|
.harness/build/
|
||||||
|
|
||||||
# phase execution outputs
|
# phase execution outputs
|
||||||
phases/**/phase*-output.json
|
phases/**/phase*-output.json
|
||||||
phases/**/step*-output.json
|
phases/**/step*-output.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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
## 프로젝트 정체성
|
## 프로젝트 정체성
|
||||||
- FESA는 유한요소법 기반 구조해석 솔버 개발 프로젝트이다.
|
- FESA는 유한요소법 기반 구조해석 솔버 개발 프로젝트이다.
|
||||||
- Harness는 솔버 자체가 아니라 요구조건, TDD, phase 실행, 검증을 통제하는 개발 운영 인프라이다.
|
- Harness는 솔버 자체가 아니라 요구조건, TDD, phase 실행, 검증을 통제하는 개발 운영 인프라이다. 전체 실행 흐름은 `docs/HARNESS_WORKFLOW.md`, 설치와 설정은 `docs/HARNESS.md`를 따른다.
|
||||||
- 문서와 구현은 full Abaqus compatibility를 주장하지 않는다. 기능별로 승인된 Abaqus keyword subset만 지원한다.
|
- 문서와 구현은 full Abaqus compatibility를 주장하지 않는다. 기능별로 승인된 Abaqus keyword subset만 지원한다.
|
||||||
- 공식 solver output은 HDF5 `results.h5`이다.
|
- 공식 solver output은 HDF5 `results.h5`이다.
|
||||||
- reference 결과는 FESA와 같은 Abaqus `.inp` 모델을 Abaqus로 해석해 생성한 CSV 파일이다.
|
- reference 결과는 FESA와 같은 Abaqus `.inp` 모델을 Abaqus로 해석해 생성한 CSV 파일이다.
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
- CSV는 FESA 공식 output이 아니며, FESA HDF5에서 추출한 deterministic CSV view는 비교 디버깅/검토용 보조 artifact로만 둔다.
|
- CSV는 FESA 공식 output이 아니며, FESA HDF5에서 추출한 deterministic CSV view는 비교 디버깅/검토용 보조 artifact로만 둔다.
|
||||||
|
|
||||||
## 아키텍처 규칙
|
## 아키텍처 규칙
|
||||||
- CRITICAL: 기본 검증 경로는 `python scripts/validate_workspace.py`이다.
|
|
||||||
- CRITICAL: C++ 빌드는 CMake/MSVC/x64/Debug 기준으로 검증한다.
|
- CRITICAL: C++ 빌드는 CMake/MSVC/x64/Debug 기준으로 검증한다.
|
||||||
- CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다.
|
- CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다.
|
||||||
- CRITICAL: C++ production file을 바꿀 때는 관련 C++ test file이 있어야 한다.
|
- CRITICAL: C++ production file을 바꿀 때는 관련 C++ test file이 있어야 한다.
|
||||||
- CRITICAL: Abaqus reference artifact 생성, 수정, 복원은 명시적으로 요청된 phase에서만 수행한다.
|
- CRITICAL: Abaqus reference artifact 생성, 수정, 복원은 명시적으로 요청된 phase에서만 수행한다.
|
||||||
- CRITICAL: `harness-workflow` 스킬은 사용자가 명시적으로 허용하기 전까지 사용하지 않는다.
|
- CRITICAL: public header와 implementation 의존성 방향을 역전하지 말 것
|
||||||
|
- CRITICAL: 사람 개발자가 검토하기 쉽도록 코드를 작성할것. 그리고 주석도 필수로 작성할 것.
|
||||||
- Domain은 입력 파일에서 생성된 전체 모델 정의를 소유하고, 파싱 이후 가능한 한 불변으로 취급한다.
|
- Domain은 입력 파일에서 생성된 전체 모델 정의를 소유하고, 파싱 이후 가능한 한 불변으로 취급한다.
|
||||||
- AnalysisModel은 현재 step에서 활성화된 elements, loads, boundary conditions, properties/materials의 view를 제공하며 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를 분산 저장하지 않는다.
|
- 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 경계 뒤에 둔다.
|
- MKL, TBB, HDF5 API는 solver core에 직접 노출하지 않는다. `LinearSolver`, `ParallelFor`, `ResultsWriter`, `Vector`, `Matrix`, `SparseMatrix` adapter 경계 뒤에 둔다.
|
||||||
- Codex custom agent의 `model_reasoning_effort` 기본값은 `extra high`로 둔다.
|
- Codex custom agent의 `model_reasoning_effort` 기본값은 `extra high`로 둔다.
|
||||||
- Harness runner는 `scripts/execute.py`에 둔다.
|
- Harness runner는 `scripts/execute.py`에 둔다.
|
||||||
- `scripts/execute.py`는 `codex/<phase-name>` branch prefix만 사용한다.
|
- `scripts/execute.py`는 `feat-<phase-name>` branch prefix를 사용한다.
|
||||||
- `scripts/execute.py` 실행 전 worktree는 clean 상태여야 한다.
|
- runner는 `git add -A`로 변경사항을 stage하므로 실행 전 clean worktree 또는 별도 Git worktree를 사용한다.
|
||||||
- 각 phase step은 non-empty `allowed_paths`를 선언해야 한다.
|
- Hook 연결은 `.codex/hooks.json`, 구현은 `scripts/hooks/`와 `scripts/msvc_harness/`에 둔다.
|
||||||
- runner는 explicit allowed path와 runner housekeeping file만 stage하며 broad staging을 사용하지 않는다.
|
- PreToolUse는 위험 명령과 C++ production file의 대응 테스트 존재 여부를 검사하는 guardrail이며 RED 실행을 증명하지 않는다.
|
||||||
- runner가 만드는 모든 commit 전에는 Harness Python self-test와 `python scripts/validate_workspace.py`가 통과해야 한다.
|
- Stop은 `.harness/config.json` 또는 자동 감지 결과에 따라 MSVC build와 test를 모두 검증한다.
|
||||||
- Codex hook 정책은 `.codex/hooks/`에 둔다.
|
|
||||||
- Generated phase execution outputs remain ignored under `phases/**/step*-output.json`.
|
- Generated phase execution outputs remain ignored under `phases/**/step*-output.json`.
|
||||||
|
|
||||||
## 개발 프로세스
|
## 개발 프로세스
|
||||||
- TDD를 기본으로 한다. 구현은 `RED -> GREEN -> VERIFY` 순서를 따른다.
|
- TDD를 기본으로 한다. 구현은 `RED -> GREEN -> VERIFY` 순서를 따른다.
|
||||||
|
- CRITICAL: 빌드 경고를 새로 추가하지 말 것.
|
||||||
- 기능 개발은 다음 gate를 순서대로 통과해야 한다.
|
- 기능 개발은 다음 gate를 순서대로 통과해야 한다.
|
||||||
1. 요구조건 분석
|
1. 요구조건 분석
|
||||||
2. 연구자료 조사
|
2. 연구자료 조사
|
||||||
@@ -54,10 +54,7 @@
|
|||||||
9. reference comparison
|
9. reference comparison
|
||||||
10. physics sanity
|
10. physics sanity
|
||||||
11. release readiness
|
11. release readiness
|
||||||
- 커밋 전 hook은 Harness Python self-test와 workspace validation을 실행해야 한다.
|
- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`.
|
||||||
- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`.
|
|
||||||
- Codex는 작업 완료 후 검증을 마치면 즉시 변경사항을 commit하고 push한다.
|
|
||||||
- 계획이 필요한 장기 작업은 Harness phase로 나누고, 각 step은 독립 실행 가능해야 한다.
|
|
||||||
|
|
||||||
## Agent/Skill Workflow
|
## Agent/Skill Workflow
|
||||||
| 개발 과정 | Agent | Skill | 산출물 |
|
| 개발 과정 | Agent | Skill | 산출물 |
|
||||||
@@ -76,22 +73,35 @@
|
|||||||
| 배포 준비 | `release-agent` | `fesa-release-readiness` | `docs/releases/<feature-id>-release.md` |
|
| 배포 준비 | `release-agent` | `fesa-release-readiness` | `docs/releases/<feature-id>-release.md` |
|
||||||
|
|
||||||
## 명령어
|
## 명령어
|
||||||
```bash
|
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
### Harness Python 검증
|
||||||
python scripts/validate_workspace.py
|
|
||||||
python scripts/execute.py <phase-dir>
|
```powershell
|
||||||
python scripts/execute.py <phase-dir> --push
|
uv run --with pytest python -m pytest -v -rs
|
||||||
```
|
```
|
||||||
|
|
||||||
## MSVC 검증 기본값
|
### Phase 실행
|
||||||
- Generator: `Visual Studio 17 2022`
|
|
||||||
- Platform: `x64`
|
|
||||||
- Config: `Debug`
|
|
||||||
- Build directory: `build/msvc-debug`
|
|
||||||
|
|
||||||
Override variables:
|
```powershell
|
||||||
- `HARNESS_VALIDATION_COMMANDS`
|
python scripts/execute.py <phase-name>
|
||||||
- `HARNESS_CMAKE_GENERATOR`
|
python scripts/execute.py <phase-name> --push
|
||||||
- `HARNESS_CMAKE_PLATFORM`
|
```
|
||||||
- `HARNESS_CMAKE_CONFIG`
|
|
||||||
- `HARNESS_BUILD_DIR`
|
### 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`과 프로젝트 문서를
|
||||||
|
우선한다.
|
||||||
|
|||||||
+14
-7
@@ -6,18 +6,18 @@ FESA의 architecture decision은 solver correctness, verification traceability,
|
|||||||
---
|
---
|
||||||
|
|
||||||
### ADR-001: FESA는 구조해석 솔버 프로젝트이고 Harness는 운영 인프라로 둔다
|
### 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 중심이면 agent가 solver architecture, FEM verification, Abaqus/HDF5 계약보다 운영 스크립트에 과도하게 맞춰 행동한다.
|
||||||
|
|
||||||
**트레이드오프**: Harness 문서의 비중은 낮아지지만, 검증 명령과 hook 정책은 계속 필수 운영 규칙으로 유지한다.
|
**트레이드오프**: Harness 문서의 비중은 낮아지지만, `docs/HARNESS_WORKFLOW.md`의 실행 계약과 `.codex/hooks.json`의 검증 정책은 계속 필수 운영 규칙으로 유지한다.
|
||||||
|
|
||||||
### ADR-002: C++17/MSVC/CMake/CTest를 기본 구현 환경으로 둔다
|
### ADR-002: C++17/MSVC/CMake/CTest를 기본 구현 환경으로 둔다
|
||||||
**결정**: 기본 solver 구현과 validation은 C++17 이상, Visual Studio 17 2022 generator, x64 platform, Debug config, 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다.
|
**이유**: 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을 지원한다
|
### ADR-003: Abaqus `.inp` full compatibility가 아니라 기능별 keyword subset을 지원한다
|
||||||
**결정**: FESA parser는 Abaqus keyword/data/comment line 규칙을 따르되, 기능별로 승인된 keyword subset만 지원한다. 미지원 keyword는 명확한 diagnostic을 남긴다.
|
**결정**: FESA parser는 Abaqus keyword/data/comment line 규칙을 따르되, 기능별로 승인된 keyword subset만 지원한다. 미지원 keyword는 명확한 diagnostic을 남긴다.
|
||||||
@@ -75,9 +75,16 @@ FESA의 architecture decision은 solver correctness, verification traceability,
|
|||||||
|
|
||||||
**트레이드오프**: reference 준비가 느려질 수 있다. 대신 검증 기준의 신뢰도와 감사 가능성이 높아진다.
|
**트레이드오프**: reference 준비가 느려질 수 있다. 대신 검증 기준의 신뢰도와 감사 가능성이 높아진다.
|
||||||
|
|
||||||
### ADR-011: C++ production 변경은 TDD guard와 workspace validation을 통과해야 한다
|
### ADR-011: 구형 단일 검증 진입점 계약을 폐기한다
|
||||||
**결정**: C++ production file 변경은 관련 C++ test file이 없으면 차단한다. 기본 검증은 `python -m unittest discover -s scripts -p "test_*.py"`와 `python scripts/validate_workspace.py`를 사용한다.
|
|
||||||
|
|
||||||
**이유**: 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로 유지한다.
|
||||||
|
|||||||
+22
-49
@@ -9,7 +9,7 @@ FESA의 아키텍처 목표는 Abaqus `.inp` subset을 내부 semantic model로
|
|||||||
- sparse linear algebra backend isolation
|
- sparse linear algebra backend isolation
|
||||||
- deterministic verification
|
- deterministic verification
|
||||||
- incremental feature addition
|
- incremental feature addition
|
||||||
- Harness 기반 TDD와 workspace validation
|
- Harness 기반 TDD
|
||||||
|
|
||||||
## 디렉토리 구조
|
## 디렉토리 구조
|
||||||
```text
|
```text
|
||||||
@@ -31,6 +31,9 @@ src/
|
|||||||
analysis/ # static, modal, dynamic, nonlinear procedure drivers
|
analysis/ # static, modal, dynamic, nonlinear procedure drivers
|
||||||
results/ # recovery, field/history output, diagnostics
|
results/ # recovery, field/history output, diagnostics
|
||||||
validation/ # comparison metrics and tolerance helpers
|
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/
|
tests/
|
||||||
unit/
|
unit/
|
||||||
integration/
|
integration/
|
||||||
@@ -43,24 +46,29 @@ reference/
|
|||||||
<model-id>_reactions.csv
|
<model-id>_reactions.csv
|
||||||
<model-id>_internalforces.csv
|
<model-id>_internalforces.csv
|
||||||
<model-id>_stresses.csv
|
<model-id>_stresses.csv
|
||||||
|
.agents/
|
||||||
|
skills/ # Harness and review skills
|
||||||
.codex/
|
.codex/
|
||||||
hooks/ # Codex hook scripts
|
hooks.json # PreToolUse/Stop hook registration
|
||||||
skills/ # FESA solver and Harness instructions
|
agents/ # FESA workflow custom agents
|
||||||
|
skills/ # FESA solver workflow skills
|
||||||
docs/ # Product, architecture, ADR, workflow artifacts
|
docs/ # Product, architecture, ADR, workflow artifacts
|
||||||
scripts/
|
scripts/
|
||||||
execute.py # Phase step executor
|
execute.py # Phase step executor
|
||||||
validate_workspace.py # Default validation entry point
|
hooks/ # PreToolUse/Stop hook implementations
|
||||||
test_*.py # Harness self-tests
|
msvc_harness/ # MSVC project discovery and validation adapters
|
||||||
phases/ # Optional generated phase plans
|
phases/ # Optional generated phase plans
|
||||||
```
|
```
|
||||||
|
|
||||||
## Harness Execution Layer
|
## Harness Execution Layer
|
||||||
`scripts/execute.py`:
|
|
||||||
- creates or checks out `codex/<phase-name>`
|
Harness는 solver core와 분리된 세 계층의 개발 운영 인프라다.
|
||||||
- refuses to run on a dirty worktree
|
|
||||||
- requires per-step `allowed_paths`
|
- 계획 계층: `.agents/skills/harness`가 사용자 승인 전 Step 초안을 만들고, 승인 후 `phases/` 파일을 생성한다.
|
||||||
- stages only explicit allowed paths and runner housekeeping files
|
- 실행 계층: `scripts/execute.py`가 `feat-<phase-name>` 브랜치에서 Step마다 독립 Codex 세션을 실행하고 상태와 커밋을 관리한다.
|
||||||
- runs Python Harness self-tests and workspace validation before every runner-created commit
|
- 검증 계층: `.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`는 외부 라이브러리에 의존하지 않는다.
|
- `core`는 외부 라이브러리에 의존하지 않는다.
|
||||||
@@ -148,6 +156,9 @@ Results
|
|||||||
├── ResultFrame
|
├── ResultFrame
|
||||||
├── FieldOutput
|
├── FieldOutput
|
||||||
└── HistoryOutput
|
└── HistoryOutput
|
||||||
|
|
||||||
|
Vector
|
||||||
|
Matrix
|
||||||
```
|
```
|
||||||
|
|
||||||
## 상태 관리
|
## 상태 관리
|
||||||
@@ -231,41 +242,3 @@ Schema requirements:
|
|||||||
- Abaqus reference results는 `reference/<model-id>/` 아래 CSV 파일이다.
|
- Abaqus reference results는 `reference/<model-id>/` 아래 CSV 파일이다.
|
||||||
- Verification은 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준으로 FESA HDF5 rows와 Abaqus reference CSV rows를 비교한다.
|
- 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가 아니다.
|
- 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
|
|
||||||
```
|
|
||||||
|
|||||||
+124
@@ -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 <phase-name>
|
||||||
|
python scripts/execute.py <phase-name> --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는
|
||||||
|
별도의 로그 파일을 만들지 않는다.
|
||||||
@@ -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 <repository-root>
|
||||||
|
-
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <source> -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 <configurePreset>
|
||||||
|
cmake --build --preset <buildPreset>
|
||||||
|
ctest --preset <testPreset> --show-only=json-v1
|
||||||
|
ctest --preset <testPreset> --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
이 경우 모든 명령은 `cmake.sourceDir`에서 실행하고 compiler metadata 검사는 설정한
|
||||||
|
`binaryDir`에서 수행한다.
|
||||||
|
|
||||||
|
### 9.3 직접 MSBuild
|
||||||
|
|
||||||
|
[MSBuild adapter](../scripts/msvc_harness/adapters/msbuild.py)는 다음 순서로 실행한다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
MSBuild.exe <solution-or-vcxproj> /m /nologo `
|
||||||
|
/p:Configuration=<configuration> `
|
||||||
|
/p:Platform=<platform>
|
||||||
|
|
||||||
|
<msbuild.testCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
직접 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가 남아 있으면 실제 작업 지시와 충돌할 수 있다.
|
||||||
+8
-16
@@ -20,11 +20,11 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
|
|||||||
6. `LinearSolver` adapter를 통한 MKL PARDISO backend와 향후 iterative solver 확장
|
6. `LinearSolver` adapter를 통한 MKL PARDISO backend와 향후 iterative solver 확장
|
||||||
7. HDF5 기반 `ResultStep` -> `ResultFrame` -> `FieldOutput`/`HistoryOutput` 저장
|
7. HDF5 기반 `ResultStep` -> `ResultFrame` -> `FieldOutput`/`HistoryOutput` 저장
|
||||||
8. FESA HDF5 rows와 `reference/<model-id>/` 아래 Abaqus reference CSV rows의 직접 비교
|
8. FESA HDF5 rows와 `reference/<model-id>/` 아래 Abaqus reference CSV rows의 직접 비교
|
||||||
9. CMake/MSVC/x64/Debug, CTest, Harness validation, TDD guard 기반 개발 검증
|
9. CMake/MSVC/x64/Debug, CTest 기반 개발 검증
|
||||||
|
|
||||||
## V0 범위
|
## V0 범위
|
||||||
- 선형 정적 해석 골격
|
- 선형 정적 해석 파이프라인 구현
|
||||||
- 첫 end-to-end 기능 후보: 1D truss/bar element
|
- 첫 end-to-end 기능 후보: Isoparametric 3D Euler beam element
|
||||||
- 최소 Abaqus keyword subset:
|
- 최소 Abaqus keyword subset:
|
||||||
- `*HEADING`
|
- `*HEADING`
|
||||||
- `*NODE`
|
- `*NODE`
|
||||||
@@ -33,24 +33,17 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
|
|||||||
- `*ELSET`
|
- `*ELSET`
|
||||||
- `*MATERIAL`
|
- `*MATERIAL`
|
||||||
- `*ELASTIC`
|
- `*ELASTIC`
|
||||||
- section keyword
|
- `*Beam General Section`
|
||||||
- `*BOUNDARY`
|
- `*BOUNDARY`
|
||||||
- `*CLOAD`
|
- `*CLOAD`
|
||||||
- `*STEP`
|
- `*STEP`
|
||||||
- `*STATIC`
|
- `*STATIC`
|
||||||
- output request subset
|
- output request subset
|
||||||
- displacement 중심의 최소 `AnalysisState`
|
- MKL PARDISO 기반 sparse direct solver
|
||||||
|
- displacements, reactions, elemental forces 중심의 verification
|
||||||
- HDF5 result schema v0
|
- HDF5 result schema v0
|
||||||
- FESA HDF5 to Abaqus reference CSV comparison 계약
|
- 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 |
|
| 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-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-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-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-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 |
|
| 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 근거가 신뢰도와 한계와 함께 정리되어 있다.
|
2. Research evidence complete: 정식화와 benchmark 근거가 신뢰도와 한계와 함께 정리되어 있다.
|
||||||
3. Formulation reviewed: 약형, shape function, B matrix, constitutive contract, 수치적분, output recovery가 검토되어 있다.
|
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가 승인되어 있다.
|
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가 통과한다.
|
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 기준 안에 있다.
|
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가 검토되어 있다.
|
8. Physics sanity pass: equilibrium, reaction consistency, displacement direction, symmetry, stress sanity가 검토되어 있다.
|
||||||
@@ -92,4 +85,3 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
|
|||||||
- GUI 또는 postprocessor
|
- GUI 또는 postprocessor
|
||||||
- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow
|
- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow
|
||||||
- Explicit dynamics, contact, plasticity, shell end-to-end 구현
|
- Explicit dynamics, contact, plasticity, shell end-to-end 구현
|
||||||
- JavaScript/TypeScript fallback 유지
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- Abaqus나 Nastran을 Agent가 직접 실행하지 않는다. `reference/<model-id>/`에 저장된 `model.inp`, `metadata.json`, Abaqus reference CSV files를 검증 기준으로 사용한다.
|
- Abaqus나 Nastran을 Agent가 직접 실행하지 않는다. `reference/<model-id>/`에 저장된 `model.inp`, `metadata.json`, Abaqus reference CSV files를 검증 기준으로 사용한다.
|
||||||
- 기본 개발 환경은 C++17 이상, MSVC, CMake, CTest이다.
|
- 기본 개발 환경은 C++17 이상, MSVC, CMake, CTest이다.
|
||||||
- 모든 기능은 tolerance 기준을 명시하고, 기준을 만족할 때만 배포 후보가 된다.
|
- 모든 기능은 tolerance 기준을 명시하고, 기준을 만족할 때만 배포 후보가 된다.
|
||||||
|
- Harness 운영은 `docs/HARNESS_WORKFLOW.md`의 계획, 독립 Step 실행, PreToolUse/Stop 검증 계층을 따른다.
|
||||||
|
|
||||||
## 전체 Agent 구성
|
## 전체 Agent 구성
|
||||||
|
|
||||||
@@ -168,15 +169,20 @@ C++ 코드를 구현하는 Agent이다.
|
|||||||
빌드와 테스트를 실행하는 Agent이다.
|
빌드와 테스트를 실행하는 Agent이다.
|
||||||
|
|
||||||
책임:
|
책임:
|
||||||
- Harness validation을 실행한다.
|
- `.harness/config.json` 또는 자동 감지 결과에 맞는 MSVC build/test 명령을 실행한다.
|
||||||
- MSVC x64 Debug CMake configure/build/CTest 결과를 수집한다.
|
- MSVC x64 Debug CMake configure/build/CTest 결과를 수집한다.
|
||||||
- 실패 로그를 요약하고 Correction Agent에 전달한다.
|
- 실패 로그를 요약하고 Correction Agent에 전달한다.
|
||||||
|
|
||||||
기본 검증 명령:
|
기본 CMake 검증 명령:
|
||||||
```powershell
|
```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
|
- CMake configure
|
||||||
- MSVC Debug build
|
- MSVC Debug build
|
||||||
@@ -301,7 +307,8 @@ flowchart TD
|
|||||||
통과 조건:
|
통과 조건:
|
||||||
- CMake/MSVC/CTest validation이 통과한다.
|
- CMake/MSVC/CTest validation이 통과한다.
|
||||||
- 단위 테스트와 통합 테스트가 통과한다.
|
- 단위 테스트와 통합 테스트가 통과한다.
|
||||||
- Harness TDD guard를 만족한다.
|
- 관련 C++ test file이 있고 같은 구현 Step 안에 RED 실패와 후속 GREEN 성공 증거가 있다.
|
||||||
|
- Stop의 전체 MSVC build/test 검증이 통과한다.
|
||||||
|
|
||||||
### Gate 5: 레퍼런스 검증
|
### Gate 5: 레퍼런스 검증
|
||||||
통과 조건:
|
통과 조건:
|
||||||
@@ -352,11 +359,12 @@ Coordinator Agent는 분류 결과에 따라 Requirement, Formulation, I/O Defin
|
|||||||
## 초기 적용 우선순위
|
## 초기 적용 우선순위
|
||||||
|
|
||||||
1. 선형 정적 해석의 최소 골격
|
1. 선형 정적 해석의 최소 골격
|
||||||
2. 1D truss 또는 bar element
|
2. Isoparametric 3D Euler beam element
|
||||||
3. 2D plane stress/plane strain element
|
3. 1D truss 또는 bar element
|
||||||
4. 3D solid element
|
4. 2D plane stress/plane strain element
|
||||||
5. material model 확장
|
5. 3D solid element
|
||||||
6. nonlinear 또는 dynamic analysis 확장
|
6. material model 확장
|
||||||
|
7. nonlinear 또는 dynamic analysis 확장
|
||||||
|
|
||||||
각 단계는 요구조건, 정식화, 테스트모델, 구현, 레퍼런스 비교, 배포 Gate를 독립적으로 통과해야 한다.
|
각 단계는 요구조건, 정식화, 테스트모델, 구현, 레퍼런스 비교, 배포 Gate를 독립적으로 통과해야 한다.
|
||||||
|
|
||||||
|
|||||||
+20
-14
@@ -15,7 +15,8 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로
|
|||||||
- Abaqus, Nastran 또는 reference solver 실행은 skill 범위에 포함하지 않는다.
|
- Abaqus, Nastran 또는 reference solver 실행은 skill 범위에 포함하지 않는다.
|
||||||
- Abaqus reference CSV 파일 생성/수정은 skill 범위에 포함하지 않는다.
|
- Abaqus reference CSV 파일 생성/수정은 skill 범위에 포함하지 않는다.
|
||||||
- C++ 구현 관련 skill은 C++17 이상, MSVC, CMake, CTest, TDD 원칙을 따른다.
|
- 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 구성
|
## 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`를 작성한다.
|
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를 정리한다.
|
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` 여부를 판단한다.
|
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를 정의한다.
|
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
|
```powershell
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
cmake -S . -B .harness/build -A x64
|
||||||
python scripts/validate_workspace.py
|
cmake --build .harness/build --config Debug
|
||||||
ctest -C Debug -R <feature-or-label>
|
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --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`로 분류한다.
|
- 실패는 `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`로 분류한다.
|
||||||
@@ -148,7 +157,8 @@ ctest -C Debug -R <feature-or-label>
|
|||||||
|
|
||||||
## 검증 기준
|
## 검증 기준
|
||||||
|
|
||||||
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
|
```powershell
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
uv run --with pytest python -m pytest -v -rs
|
||||||
python scripts/validate_workspace.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Skill 구조 검증:
|
개별 skill schema를 점검할 때는 현재 Codex 설치에 포함된 `skill-creator` validator를
|
||||||
|
사용하되 사용자 홈을 하드코딩한 경로를 프로젝트 계약으로 두지 않는다.
|
||||||
```powershell
|
|
||||||
python C:\Users\user\.codex\skills\.system\skill-creator\scripts\quick_validate.py .codex\skills\<skill-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
## v1 범위
|
## v1 범위
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# FESA 초기 문서 완성 계획 노트
|
# FESA 초기 문서 완성 계획 노트
|
||||||
|
|
||||||
|
> **Historical / Superseded:** 이 문서는 2026-06-10 시점의 초기 조사와 실행 기록이다.
|
||||||
|
> 현재 제품 범위는 `docs/PRD.md`, Harness 운영 계약은 `docs/HARNESS_WORKFLOW.md`와
|
||||||
|
> `docs/HARNESS.md`를 따른다. 아래의 구형 스킬명, 검증 명령, 기능 우선순위는 현재
|
||||||
|
> 지침으로 사용하지 않는다.
|
||||||
|
|
||||||
## 메타데이터
|
## 메타데이터
|
||||||
- 작성일: 2026-06-10
|
- 작성일: 2026-06-10
|
||||||
- 목적: `AGENTS.md`, `docs/PRD.md`, `docs/ARCHITECTURE.md`를 유한요소법 기반 구조해석 솔버 개발 프로젝트 문서로 완성하기 위한 조사 내용과 실행 계획 정리
|
- 목적: `AGENTS.md`, `docs/PRD.md`, `docs/ARCHITECTURE.md`를 유한요소법 기반 구조해석 솔버 개발 프로젝트 문서로 완성하기 위한 조사 내용과 실행 계획 정리
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
이 디렉터리는 Build/Test Executor Agent가 작성하거나 제안하는 기능별 build/test 실행 리포트를 보관하는 위치다.
|
이 디렉터리는 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/<feature-id>-build-test.md` 형식을 사용한다.
|
기본 문서명은 `docs/build-test-reports/<feature-id>-build-test.md` 형식을 사용한다.
|
||||||
|
|
||||||
## Build/Test Executor Agent 역할
|
## Build/Test Executor Agent 역할
|
||||||
|
|
||||||
수행한다:
|
수행한다:
|
||||||
- `python scripts/validate_workspace.py`를 기본 검증 명령으로 실행한다.
|
- `.harness/config.json`과 프로젝트 자동 감지 결과를 확인하고 같은 build/test 경로를 독립 실행한다.
|
||||||
- implementation plan/report에 명시된 경우 harness self-test와 feature-specific CTest를 실행한다.
|
- implementation plan/report에 명시된 feature-specific CTest를 전체 검증 전에 실행한다.
|
||||||
- `HARNESS_VALIDATION_COMMANDS`, `CMakePresets.json`의 `msvc-debug`, 기본 CMake/MSVC x64 Debug 경로 중 어떤 검증 경로가 사용되었는지 기록한다.
|
- 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 실패를 구분한다.
|
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
|
||||||
- command, exit code, duration, stdout/stderr tail, failed test name을 요약한다.
|
- command, exit code, duration, stdout/stderr tail, failed test name을 요약한다.
|
||||||
- 실패 원인에 따라 Implementation Agent, Correction Agent, Reference Verification Agent, Implementation Planning Agent 중 handoff 대상을 제안한다.
|
- 실패 원인에 따라 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
|
```powershell
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
cmake -S . -B .harness/build -A x64
|
||||||
ctest -C Debug -R <feature-or-label>
|
cmake --build .harness/build --config Debug
|
||||||
python scripts/validate_workspace.py
|
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --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`
|
```powershell
|
||||||
2. `CMakePresets.json`의 `msvc-debug`
|
uv run --with pytest python -m pytest -v -rs
|
||||||
3. 기본 CMake/MSVC x64 Debug 명령
|
```
|
||||||
4. `CMakeLists.txt`가 없고 override도 없으면 안내 메시지와 함께 성공 종료
|
|
||||||
|
프로젝트 선택 우선순위는 다음과 같다.
|
||||||
|
|
||||||
|
1. `.harness/config.json`의 명시적 `projectType`
|
||||||
|
2. 루트의 CMake metadata
|
||||||
|
3. 루트의 단일 `.sln`
|
||||||
|
4. 루트의 단일 `.vcxproj`
|
||||||
|
|
||||||
기본 CMake/MSVC x64 Debug 명령은 다음과 같다.
|
기본 CMake/MSVC x64 Debug 명령은 다음과 같다.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
|
cmake -S . -B .harness/build -A x64
|
||||||
cmake --build build/msvc-debug --config Debug
|
cmake --build .harness/build --config Debug
|
||||||
ctest --test-dir build/msvc-debug --output-on-failure -C 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
|
```markdown
|
||||||
@@ -69,17 +84,19 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug
|
|||||||
- generator: Visual Studio 17 2022 | <observed generator>
|
- generator: Visual Studio 17 2022 | <observed generator>
|
||||||
- platform: x64 | <observed platform>
|
- platform: x64 | <observed platform>
|
||||||
- config: Debug | <observed config>
|
- config: Debug | <observed config>
|
||||||
- build_dir: build/msvc-debug | <observed build dir>
|
- build_dir: .harness/build | <configured/observed build dir>
|
||||||
- active_override_env_vars: HARNESS_VALIDATION_COMMANDS | HARNESS_CMAKE_GENERATOR | HARNESS_CMAKE_PLATFORM | HARNESS_CMAKE_CONFIG | HARNESS_BUILD_DIR | none
|
- harness_config: .harness/config.json | absent-defaults
|
||||||
- command_discovery_path: HARNESS_VALIDATION_COMMANDS | CMakePresets.json msvc-debug | default CMake/MSVC x64 Debug | no-CMake informational success
|
- 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
|
## Command Log Summary
|
||||||
|
|
||||||
| order | command | exit_code | duration | stdout_stderr_tail |
|
| order | command | exit_code | duration | stdout_stderr_tail |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| 1 | python -m unittest discover -s scripts -p "test_*.py" | <code> | <duration> | <tail summary> |
|
| 1 | uv run --with pytest python -m pytest -v -rs (when applicable) | <code or skipped> | <duration> | <tail summary> |
|
||||||
| 2 | ctest -C Debug -R <feature-or-label> | <code> | <duration> | <tail summary> |
|
| 2 | <config-resolved configure/build commands> | <code> | <duration> | <tail summary> |
|
||||||
| 3 | python scripts/validate_workspace.py | <code> | <duration> | <tail summary> |
|
| 3 | <feature-specific test command when applicable> | <code or skipped> | <duration> | <tail summary> |
|
||||||
|
| 4 | <config-resolved test discovery/full test commands> | <code> | <duration> | <tail summary> |
|
||||||
|
|
||||||
## Validation Results
|
## Validation Results
|
||||||
|
|
||||||
@@ -137,7 +154,7 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug
|
|||||||
- 모든 실행 명령과 exit code를 기록해야 한다.
|
- 모든 실행 명령과 exit code를 기록해야 한다.
|
||||||
- 실패 로그는 전체 원문을 복제하지 않고 마지막 핵심 구간과 실패 원인을 요약한다.
|
- 실패 로그는 전체 원문을 복제하지 않고 마지막 핵심 구간과 실패 원인을 요약한다.
|
||||||
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
|
- 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 통과까지만 의미한다.
|
- 성공 판정은 build/test 통과까지만 의미한다.
|
||||||
- reference tolerance, physics validation, release readiness는 판정하지 않는다.
|
- reference tolerance, physics validation, release readiness는 판정하지 않는다.
|
||||||
- upstream 계약 문제는 Implementation Agent에 임의 수정으로 넘기지 않고 적절한 upstream agent로 handoff한다.
|
- upstream 계약 문제는 Implementation Agent에 임의 수정으로 넘기지 않고 적절한 upstream agent로 handoff한다.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT
|
|||||||
- Reference Verification Agent report
|
- Reference Verification Agent report
|
||||||
- Physics Evaluation Agent report
|
- Physics Evaluation Agent report
|
||||||
- Release 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로 수행한다.
|
- Coordinator Agent config와 문서 템플릿에 자동화된 Python 검증이 있으면 `uv run --with pytest python -m pytest -v -rs`로 실행한다.
|
||||||
- workspace 검증은 `python scripts/validate_workspace.py`를 사용한다.
|
- C++ build/test evidence는 `.harness/config.json` 또는 Harness 자동 감지 기본값에 따른 Build/Test report에서 확인한다.
|
||||||
- 현재 repository에 CMake 프로젝트가 없으면 harness 정책에 따라 no-CMake validation 경로가 성공으로 기록될 수 있다.
|
- C/C++ 파일과 build metadata가 모두 없을 때만 `no C/C++ project` 성공을 허용한다. C/C++ 파일만 있고 build metadata가 없으면 통과 evidence로 사용하지 않는다.
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ Correction Agent는 Build/Test Executor Agent, Reference Verification Agent, Phy
|
|||||||
- 실패 로그와 implementation report를 읽고 failure classification을 먼저 확정한다.
|
- 실패 로그와 implementation report를 읽고 failure classification을 먼저 확정한다.
|
||||||
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
|
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
|
||||||
- implementation-owned failure에 한해 source/header/test/CMake를 최소 수정한다.
|
- implementation-owned failure에 한해 source/header/test/CMake를 최소 수정한다.
|
||||||
- 수정 후 targeted command를 먼저 실행하고 `python scripts/validate_workspace.py`를 실행한다.
|
- 수정 후 targeted command를 먼저 실행하고 `.harness/config.json` 또는 자동 감지 기본값에 따른 전체 MSVC build/test를 실행한다.
|
||||||
- harness, hook, agent config 관련 수정에서는 `python -m unittest discover -s scripts -p "test_*.py"`도 실행한다.
|
- Harness Python, Hook, agent config 관련 수정에서는 `uv run --with pytest python -m pytest -v -rs`도 실행한다.
|
||||||
- 반복 실패 또는 upstream 계약 문제를 Coordinator Agent나 관련 upstream agent로 handoff한다.
|
- 반복 실패 또는 upstream 계약 문제를 Coordinator Agent나 관련 upstream agent로 handoff한다.
|
||||||
|
|
||||||
수행하지 않는다:
|
수행하지 않는다:
|
||||||
@@ -40,11 +40,16 @@ TRIAGE -> MINIMAL FIX -> VERIFY -> REPORT
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
<targeted command that reproduced the failure>
|
<targeted command that reproduced the failure>
|
||||||
python scripts/validate_workspace.py
|
cmake -S . -B .harness/build -A x64
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
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
|
## Failure Classification
|
||||||
|
|
||||||
@@ -53,7 +58,7 @@ python -m unittest discover -s scripts -p "test_*.py"
|
|||||||
- `link`: linker, symbol resolution, target dependency 실패
|
- `link`: linker, symbol resolution, target dependency 실패
|
||||||
- `test`: CTest, unit, integration, parser/I/O, ordinary regression test 실패
|
- `test`: CTest, unit, integration, parser/I/O, ordinary regression test 실패
|
||||||
- `reference-comparison`: 저장된 reference artifact와 deterministic comparison 실패
|
- `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 문제
|
- `environment`: MSVC, CMake, Python, path, permission, generator, local dependency 문제
|
||||||
- `upstream-contract`: requirements, formulation, I/O, reference artifact, tolerance, implementation plan 불일치 또는 누락
|
- `upstream-contract`: requirements, formulation, I/O, reference artifact, tolerance, implementation plan 불일치 또는 누락
|
||||||
|
|
||||||
@@ -104,8 +109,8 @@ Excluded files:
|
|||||||
| order | command | exit_code | result | evidence |
|
| order | command | exit_code | result | evidence |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| 1 | <targeted command> | <code> | pass | fail | <summary> |
|
| 1 | <targeted command> | <code> | pass | fail | <summary> |
|
||||||
| 2 | python scripts/validate_workspace.py | <code> | pass | fail | <summary> |
|
| 2 | <config-resolved full MSVC build/test commands> | <code> | pass | fail | <summary> |
|
||||||
| 3 | python -m unittest discover -s scripts -p "test_*.py" | <code or skipped> | pass | fail | skipped | <summary> |
|
| 3 | uv run --with pytest python -m pytest -v -rs | <code or skipped> | pass | fail | skipped | <summary> |
|
||||||
|
|
||||||
## Traceability
|
## Traceability
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정
|
|||||||
- CMake/CTest target, `add_test`, label, `ctest -C Debug` 검증 계획을 정의한다.
|
- CMake/CTest target, `add_test`, label, `ctest -C Debug` 검증 계획을 정의한다.
|
||||||
- candidate source/header/test/CMake 파일과 ownership boundary를 제안한다.
|
- candidate source/header/test/CMake 파일과 ownership boundary를 제안한다.
|
||||||
- requirement, task, test, reference model, acceptance criterion을 Acceptance Traceability Matrix로 연결한다.
|
- 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++ 코드를 구현하지 않는다.
|
- C++ 코드를 구현하지 않는다.
|
||||||
@@ -80,8 +80,8 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정
|
|||||||
- add_test_needs: <CTest registration needs>
|
- add_test_needs: <CTest registration needs>
|
||||||
- labels: unit | integration | reference | parser | io
|
- labels: unit | integration | reference | parser | io
|
||||||
- msvc_config: Debug
|
- msvc_config: Debug
|
||||||
- expected_feature_command: ctest -C Debug -R <feature-or-label>
|
- expected_feature_command: ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
|
||||||
- workspace_validation: python scripts/validate_workspace.py
|
- full_validation_source: .harness/config.json | Harness auto detection
|
||||||
|
|
||||||
## Candidate Files and Ownership
|
## Candidate Files and Ownership
|
||||||
|
|
||||||
@@ -107,11 +107,17 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정
|
|||||||
|
|
||||||
## Validation Commands
|
## Validation Commands
|
||||||
```powershell
|
```powershell
|
||||||
python -m unittest discover -s scripts -p "test_*.py"
|
cmake -S . -B .harness/build -A x64
|
||||||
python scripts/validate_workspace.py
|
cmake --build .harness/build --config Debug
|
||||||
ctest -C Debug -R <feature-or-label>
|
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --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
|
## Risks and Downstream Handoff
|
||||||
|
|
||||||
### Implementation Agent
|
### Implementation Agent
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT
|
|||||||
- Build/Test report status: `pass-for-reference-verification`
|
- Build/Test report status: `pass-for-reference-verification`
|
||||||
- Implementation report와 implementation plan의 feature scope 일치
|
- Implementation report와 implementation plan의 feature scope 일치
|
||||||
- requirements, formulations, numerical reviews, I/O definitions, reference models 문서의 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 |
|
| command_or_report | expected | observed | notes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| python scripts/validate_workspace.py | pass | <result> | <summary> |
|
| Build/Test report의 config-resolved CMake/MSVC/CTest | pass | <result> | <summary> |
|
||||||
| CMake/MSVC/CTest | pass | <result> | <summary> |
|
| Harness Python pytest (when applicable) | pass or N/A | <result> | <summary> |
|
||||||
| reference verification | pass-for-physics-evaluation | <status> | <summary> |
|
| reference verification | pass-for-physics-evaluation | <status> | <summary> |
|
||||||
| physics evaluation | pass-for-release-agent | <status> | <summary> |
|
| physics evaluation | pass-for-release-agent | <status> | <summary> |
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Harden Execute Runner Implementation Plan
|
# 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.
|
> **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.
|
**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.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Reference CSV Comparison Contract Implementation Plan
|
# 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.
|
> **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.
|
**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.
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
testpaths = tests
|
||||||
@@ -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
|
||||||
|
@@ -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,,,,,
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
+104
-185
@@ -8,10 +8,8 @@ Usage:
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import contextlib
|
import contextlib
|
||||||
import fnmatch
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -24,6 +22,10 @@ from typing import Optional
|
|||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
class CodexEnvironmentError(RuntimeError):
|
||||||
|
"""재시도로 해결할 수 없는 Codex CLI 환경 오류."""
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def progress_indicator(label: str):
|
def progress_indicator(label: str):
|
||||||
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
|
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
|
||||||
@@ -56,10 +58,6 @@ class StepExecutor:
|
|||||||
"""Phase 디렉토리 안의 step들을 순차 실행하는 하네스."""
|
"""Phase 디렉토리 안의 step들을 순차 실행하는 하네스."""
|
||||||
|
|
||||||
MAX_RETRIES = 3
|
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}"
|
FEAT_MSG = "feat({phase}): step {num} — {name}"
|
||||||
CHORE_MSG = "chore({phase}): step {num} output"
|
CHORE_MSG = "chore({phase}): step {num} output"
|
||||||
TZ = timezone(timedelta(hours=9))
|
TZ = timezone(timedelta(hours=9))
|
||||||
@@ -89,7 +87,6 @@ class StepExecutor:
|
|||||||
def run(self):
|
def run(self):
|
||||||
self._print_header()
|
self._print_header()
|
||||||
self._check_blockers()
|
self._check_blockers()
|
||||||
self._assert_clean_worktree("before branch checkout")
|
|
||||||
self._checkout_branch()
|
self._checkout_branch()
|
||||||
guardrails = self._load_guardrails()
|
guardrails = self._load_guardrails()
|
||||||
self._ensure_created_at()
|
self._ensure_created_at()
|
||||||
@@ -117,117 +114,8 @@ class StepExecutor:
|
|||||||
cmd = ["git"] + list(args)
|
cmd = ["git"] + list(args)
|
||||||
return subprocess.run(cmd, cwd=self._root, capture_output=True, text=True)
|
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):
|
def _checkout_branch(self):
|
||||||
branch = self._branch_name()
|
branch = f"feat-{self._phase_name}"
|
||||||
|
|
||||||
r = self._run_git("rev-parse", "--abbrev-ref", "HEAD")
|
r = self._run_git("rev-parse", "--abbrev-ref", "HEAD")
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
@@ -249,45 +137,28 @@ class StepExecutor:
|
|||||||
|
|
||||||
print(f" Branch: {branch}")
|
print(f" Branch: {branch}")
|
||||||
|
|
||||||
def _stage_paths(self, paths: list[str]):
|
def _commit_step(self, step_num: int, step_name: str):
|
||||||
if not paths:
|
output_rel = f"phases/{self._phase_dir_name}/step{step_num}-output.json"
|
||||||
return
|
index_rel = f"phases/{self._phase_dir_name}/index.json"
|
||||||
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: dict, step_name: str):
|
self._run_git("add", "-A")
|
||||||
step_num = step["step"]
|
self._run_git("reset", "HEAD", "--", output_rel)
|
||||||
changed = self._changed_paths()
|
self._run_git("reset", "HEAD", "--", index_rel)
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
msg = self.FEAT_MSG.format(phase=self._phase_name, num=step_num, name=step_name)
|
||||||
self._validate_before_commit(msg)
|
r = self._run_git("commit", "-m", msg)
|
||||||
self._stage_paths(allowed)
|
if r.returncode == 0:
|
||||||
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)
|
|
||||||
print(f" Commit: {msg}")
|
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)
|
msg = self.CHORE_MSG.format(phase=self._phase_name, num=step_num)
|
||||||
self._validate_before_commit(msg)
|
r = self._run_git("commit", "-m", msg)
|
||||||
self._stage_paths(housekeeping)
|
if r.returncode != 0:
|
||||||
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
|
print(f" WARN: housekeeping 커밋 실패: {r.stderr.strip()}")
|
||||||
r = self._run_git("commit", "-m", msg)
|
|
||||||
if r.returncode != 0:
|
|
||||||
print(f" ERROR: housekeeping commit failed: {r.stderr.strip()}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- top-level index ---
|
# --- top-level index ---
|
||||||
|
|
||||||
@@ -311,11 +182,16 @@ class StepExecutor:
|
|||||||
sections = []
|
sections = []
|
||||||
agents_md = ROOT / "AGENTS.md"
|
agents_md = ROOT / "AGENTS.md"
|
||||||
if agents_md.exists():
|
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"
|
docs_dir = ROOT / "docs"
|
||||||
if docs_dir.is_dir():
|
if docs_dir.is_dir():
|
||||||
for doc in sorted(docs_dir.glob("*.md")):
|
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 ""
|
return "\n\n---\n\n".join(sections) if sections else ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -330,11 +206,7 @@ class StepExecutor:
|
|||||||
return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n"
|
return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n"
|
||||||
|
|
||||||
def _build_preamble(self, guardrails: str, step_context: str,
|
def _build_preamble(self, guardrails: str, step_context: str,
|
||||||
allowed_paths: list[str],
|
|
||||||
prev_error: Optional[str] = None) -> str:
|
prev_error: Optional[str] = None) -> str:
|
||||||
commit_example = self.FEAT_MSG.format(
|
|
||||||
phase=self._phase_name, num="N", name="<step-name>"
|
|
||||||
)
|
|
||||||
retry_section = ""
|
retry_section = ""
|
||||||
if prev_error:
|
if prev_error:
|
||||||
retry_section = (
|
retry_section = (
|
||||||
@@ -345,9 +217,6 @@ class StepExecutor:
|
|||||||
f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 step을 수행하세요.\n\n"
|
f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 step을 수행하세요.\n\n"
|
||||||
f"{guardrails}\n\n---\n\n"
|
f"{guardrails}\n\n---\n\n"
|
||||||
f"{step_context}{retry_section}"
|
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"## 작업 규칙\n\n"
|
||||||
f"1. 이전 step에서 작성된 코드를 확인하고 일관성을 유지하라.\n"
|
f"1. 이전 step에서 작성된 코드를 확인하고 일관성을 유지하라.\n"
|
||||||
f"2. 이 step에 명시된 작업만 수행하라. 추가 기능이나 파일을 만들지 마라.\n"
|
f"2. 이 step에 명시된 작업만 수행하라. 추가 기능이나 파일을 만들지 마라.\n"
|
||||||
@@ -357,8 +226,8 @@ class StepExecutor:
|
|||||||
f" - AC 통과 → \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n"
|
f" - AC 통과 → \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n"
|
||||||
f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 → \"error\" + \"error_message\" 기록\n"
|
f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 → \"error\" + \"error_message\" 기록\n"
|
||||||
f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n"
|
f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n"
|
||||||
f"6. 모든 변경사항을 커밋하라:\n"
|
f"6. 변경사항을 직접 커밋하지 마라. Git 커밋과 timestamp는 실행기가 처리한다.\n\n"
|
||||||
f" {commit_example}\n\n---\n\n"
|
f"---\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Codex 호출 ---
|
# --- Codex 호출 ---
|
||||||
@@ -372,10 +241,30 @@ class StepExecutor:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
prompt = preamble + step_file.read_text(encoding="utf-8")
|
prompt = preamble + step_file.read_text(encoding="utf-8")
|
||||||
result = subprocess.run(
|
command = [
|
||||||
["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "--json", prompt],
|
"codex",
|
||||||
cwd=self._root, capture_output=True, text=True, timeout=1800,
|
"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:
|
if result.returncode != 0:
|
||||||
print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})")
|
print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})")
|
||||||
@@ -388,11 +277,28 @@ class StepExecutor:
|
|||||||
"stdout": result.stdout, "stderr": result.stderr,
|
"stdout": result.stdout, "stderr": result.stderr,
|
||||||
}
|
}
|
||||||
out_path = self._phase_dir / f"step{step_num}-output.json"
|
out_path = self._phase_dir / f"step{step_num}-output.json"
|
||||||
with open(out_path, "w", encoding="utf-8") as f:
|
self._write_json(out_path, output)
|
||||||
json.dump(output, f, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
return 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):
|
def _print_header(self):
|
||||||
@@ -436,16 +342,20 @@ class StepExecutor:
|
|||||||
for attempt in range(1, self.MAX_RETRIES + 1):
|
for attempt in range(1, self.MAX_RETRIES + 1):
|
||||||
index = self._read_json(self._index_file)
|
index = self._read_json(self._index_file)
|
||||||
step_context = self._build_step_context(index)
|
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}"
|
tag = f"Step {step_num}/{self._total - 1} ({done} done): {step_name}"
|
||||||
if attempt > 1:
|
if attempt > 1:
|
||||||
tag += f" [retry {attempt}/{self.MAX_RETRIES}]"
|
tag += f" [retry {attempt}/{self.MAX_RETRIES}]"
|
||||||
|
|
||||||
with progress_indicator(tag) as pi:
|
with progress_indicator(tag) as pi:
|
||||||
self._invoke_codex(step, preamble)
|
output = self._invoke_codex(step, preamble)
|
||||||
elapsed = int(pi.elapsed)
|
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)
|
index = self._read_json(self._index_file)
|
||||||
status = next((s.get("status", "pending") for s in index["steps"] if s["step"] == step_num), "pending")
|
status = next((s.get("status", "pending") for s in index["steps"] if s["step"] == step_num), "pending")
|
||||||
ts = self._stamp()
|
ts = self._stamp()
|
||||||
@@ -455,7 +365,7 @@ class StepExecutor:
|
|||||||
if s["step"] == step_num:
|
if s["step"] == step_num:
|
||||||
s["completed_at"] = ts
|
s["completed_at"] = ts
|
||||||
self._write_json(self._index_file, index)
|
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]")
|
print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -470,9 +380,22 @@ class StepExecutor:
|
|||||||
self._update_top_index("blocked")
|
self._update_top_index("blocked")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
runtime_error = ""
|
||||||
|
if output["exitCode"] != 0:
|
||||||
|
runtime_error = (
|
||||||
|
output.get("stderr", "").strip()
|
||||||
|
or output.get("stdout", "").strip()
|
||||||
|
)
|
||||||
err_msg = next(
|
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:
|
if attempt < self.MAX_RETRIES:
|
||||||
@@ -490,7 +413,7 @@ class StepExecutor:
|
|||||||
s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}"
|
s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}"
|
||||||
s["failed_at"] = ts
|
s["failed_at"] = ts
|
||||||
self._write_json(self._index_file, index)
|
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" ✗ Step {step_num}: {step_name} failed after {self.MAX_RETRIES} attempts [{elapsed}s]")
|
||||||
print(f" Error: {err_msg}")
|
print(f" Error: {err_msg}")
|
||||||
self._update_top_index("error")
|
self._update_top_index("error")
|
||||||
@@ -506,7 +429,6 @@ class StepExecutor:
|
|||||||
print("\n All steps completed!")
|
print("\n All steps completed!")
|
||||||
return
|
return
|
||||||
|
|
||||||
self._validate_step_allowlist(pending)
|
|
||||||
step_num = pending["step"]
|
step_num = pending["step"]
|
||||||
for s in index["steps"]:
|
for s in index["steps"]:
|
||||||
if s["step"] == step_num and "started_at" not in s:
|
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._write_json(self._index_file, index)
|
||||||
self._update_top_index("completed")
|
self._update_top_index("completed")
|
||||||
|
|
||||||
final_paths = [f"phases/{self._phase_dir_name}/index.json"]
|
self._run_git("add", "-A")
|
||||||
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)
|
|
||||||
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
|
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
|
||||||
msg = f"chore({self._phase_name}): mark phase completed"
|
msg = f"chore({self._phase_name}): mark phase completed"
|
||||||
r = self._run_git("commit", "-m", msg)
|
r = self._run_git("commit", "-m", msg)
|
||||||
if r.returncode != 0:
|
if r.returncode == 0:
|
||||||
print(f" ERROR: phase completion commit failed: {r.stderr.strip()}")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print(f" ✓ {msg}")
|
print(f" ✓ {msg}")
|
||||||
|
|
||||||
if self._auto_push:
|
if self._auto_push:
|
||||||
branch = self._branch_name()
|
branch = f"feat-{self._phase_name}"
|
||||||
r = self._run_git("push", "-u", "origin", branch)
|
r = self._run_git("push", "-u", "origin", branch)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
print(f"\n ERROR: git push 실패: {r.stderr.strip()}")
|
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")
|
parser.add_argument("--push", action="store_true", help="Push branch after completion")
|
||||||
args = parser.parse_args()
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -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<path>.+?)\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())
|
||||||
@@ -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())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .config import ConfigError, load_config
|
||||||
|
|
||||||
|
__all__ = ["ConfigError", "load_config"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .base import AdapterError, ValidationAdapter
|
||||||
|
from .cmake import CMakeAdapter
|
||||||
|
from .msbuild import MSBuildAdapter
|
||||||
|
|
||||||
|
__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"]
|
||||||
@@ -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
|
||||||
@@ -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")),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -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))
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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, ())
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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/" "<feature-id>/<model-id>/",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
SKILLS = {
|
|
||||||
"fesa-requirements-baseline": {
|
|
||||||
"description_terms": (
|
|
||||||
"Use when",
|
|
||||||
"FESA solver",
|
|
||||||
"requirements",
|
|
||||||
"acceptance criteria",
|
|
||||||
"verification matrix",
|
|
||||||
),
|
|
||||||
"body_terms": (
|
|
||||||
"docs/requirements/<feature-id>.md",
|
|
||||||
"Requirement Verification Matrix",
|
|
||||||
"shall",
|
|
||||||
"FESA-REQ-<FEATURE>-###",
|
|
||||||
"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/<feature-id>-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/<feature-id>-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/<feature-id>-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/<feature-id>-io.md",
|
|
||||||
"Abaqus Input Scope",
|
|
||||||
"Internal Model Contract",
|
|
||||||
"Output HDF5 Schema",
|
|
||||||
"FESA HDF5 to Reference CSV Comparison Schema",
|
|
||||||
"results.h5",
|
|
||||||
"reference/<model-id>/",
|
|
||||||
"*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/<feature-id>-reference-models.md",
|
|
||||||
"reference/<model-id>/",
|
|
||||||
"model.inp",
|
|
||||||
"metadata.json",
|
|
||||||
"<model-id>_displacements.csv",
|
|
||||||
"<model-id>_reactions.csv",
|
|
||||||
"<model-id>_internalforces.csv",
|
|
||||||
"<model-id>_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/<feature-id>-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/<feature-id>-reference-verification.md",
|
|
||||||
"ARTIFACT CHECK -> COMPARE -> CLASSIFY -> REPORT",
|
|
||||||
"results.h5",
|
|
||||||
"Abaqus reference CSV",
|
|
||||||
"reference/<model-id>/",
|
|
||||||
"<model-id>_displacements.csv",
|
|
||||||
"<model-id>_reactions.csv",
|
|
||||||
"<model-id>_internalforces.csv",
|
|
||||||
"<model-id>_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/<feature-id>-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/<feature-id>-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()
|
|
||||||
@@ -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/<feature-id>.md",
|
|
||||||
"docs/research/<feature-id>-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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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-id>/",
|
|
||||||
"model.inp",
|
|
||||||
"metadata.json",
|
|
||||||
"<model-id>_displacements.csv",
|
|
||||||
"<model-id>_reactions.csv",
|
|
||||||
"<model-id>_internalforces.csv",
|
|
||||||
"<model-id>_stresses.csv",
|
|
||||||
):
|
|
||||||
self.assertIn(required_text, instructions)
|
|
||||||
|
|
||||||
self.assertNotIn("reference" ".h5", instructions)
|
|
||||||
self.assertNotIn("references/" "<feature-id>/<model-id>/", 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/<model-id>/",
|
|
||||||
):
|
|
||||||
self.assertIn(required_text, guide)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -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/<model-id>/",
|
|
||||||
"<model-id>_displacements.csv",
|
|
||||||
"<model-id>_reactions.csv",
|
|
||||||
"<model-id>_internalforces.csv",
|
|
||||||
"<model-id>_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/<feature-id>-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()
|
|
||||||
@@ -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/<feature-id>-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()
|
|
||||||
@@ -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/<model-id>/",
|
|
||||||
):
|
|
||||||
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-<FEATURE>-001",
|
|
||||||
):
|
|
||||||
self.assertIn(required_text, guide)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -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/<feature-id>.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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
Reference in New Issue
Block a user