add uncommitted files

This commit is contained in:
KOKO\Mimi
2026-07-29 23:32:26 +09:00
parent fb0f8f39a0
commit f5379472ce
80 changed files with 7461 additions and 1 deletions
+172
View File
@@ -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`을 삭제한
뒤 재실행
+41
View File
@@ -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이
없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다.
+32
View File
@@ -0,0 +1,32 @@
{
"description": "Harness TDD, command safety, and MSVC C/C++ validation hooks.",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|shell_command|PowerShell|apply_patch|Edit|MultiEdit|Write",
"hooks": [
{
"type": "command",
"command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"",
"commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"",
"timeout": 30,
"statusMessage": "Checking Harness policies"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"",
"commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"",
"timeout": 1800,
"statusMessage": "Running MSVC build and tests"
}
]
}
]
}
}
+5 -1
View File
@@ -1 +1,5 @@
.vs/
__pycache__/
.pytest_cache/
*.py[cod]
.harness/build/
.worktrees/
+31
View File
@@ -0,0 +1,31 @@
{
"version": 1,
"projectType": "auto",
"cmake": {
"sourceDir": ".",
"binaryDir": "out/build/windows-debug",
"configurePreset": "windows-debug",
"buildPreset": "windows-debug",
"testPreset": "windows-debug"
},
"msbuild": {
"solution": "MyProject.sln",
"configuration": "Debug",
"platform": "x64",
"testCommand": [
"build/tests/Debug/MyProjectTests.exe"
]
},
"tdd": {
"testRoots": [
"tests"
],
"testPatterns": [
"{stem}_test.cpp",
"test_{stem}.cpp"
],
"exclude": [
"legacy/generated/**"
]
}
}
+97
View File
@@ -0,0 +1,97 @@
# FESA Repository Instructions
## 적용 범위와 기준 문서
이 파일은 저장소 전체에 적용한다.
- 제품 요구사항과 Phase 1 수용 조건은 `docs/PRD.md`를 따른다.
- 모듈 경계와 데이터 흐름은 `docs/ARCHITECTURE.md`를 따른다.
- 기술 선택과 트레이드오프는 `docs/ADR.md`를 따른다.
- Harness 실행 방법은 `docs/HARNESS.md``.agents/skills/harness/SKILL.md`를 따른다.
- 실제 빌드와 테스트 명령은 `.harness/config.json`과 CMake Preset을 우선한다.
## 기술 기준
- 언어 표준: C++20
- 컴파일러: Visual Studio 2022 MSVC v143
- 대상 플랫폼: Windows x64
- 빌드 및 테스트: CMake, CMake Presets, CTest, GoogleTest/GoogleMock
- 수치 연산 및 희소 직접해법: Intel oneAPI MKL
- 요소 계산 및 조립 병렬화: Intel oneAPI TBB
- 결과 저장: HDF5
- 외부 라이브러리는 개발 환경에 사전 설치된 버전을 사용한다.
- FESA는 단위 변환을 수행하지 않는다. 입력은 일관 단위계를 사용해야 한다.
## Phase 1 범위
- Abaqus `.inp` 제한 부분집합으로 작성된 flat/orphan mesh 또는 좌표변환이 없는
단일 Part/Assembly/Instance 모델을 읽는다.
- 단일 선형 정적 `*STEP`만 실행한다.
- 절점당 6자유도를 갖는 2절점 3D Isoparametric Timoshenko Beam만 구현한다.
- 등방성 선형 탄성, 일반 단면, `*BOUNDARY`, `*CLOAD`만 지원한다.
- 다른 요소, 다중 step, 여러 Instance, Instance 좌표변환, MPC, 분포하중, 비선형,
동적, 모달, 좌굴 및 열전달을 선행 구현하지 않는다.
## 아키텍처 규칙
- public header는 `include/fesa/`, 구현은 `src/fesa/`, 테스트는 `tests/`에 둔다.
- `core`, `model`, `fem`, `elements`는 Abaqus, MKL, TBB 및 HDF5 API에 의존하지 않는다.
- `io/abaqus`는 입력 syntax와 semantic mapping만 담당하며 해석 알고리즘을 알지 않는다.
- `model`에는 Abaqus keyword 문자열 대신 solver semantic model을 저장한다.
- 자유도와 equation ID는 `DofManager`가 소유한다. `Node``Element`에 분산 저장하지 않는다.
- 외부 라이브러리 handle과 resource는 adapter와 RAII wrapper 내부에 가둔다.
- 테스트 helper가 production parser, model validation 또는 solver 경로를 우회하지 않게 한다.
- 실제 두 번째 구현이 생기기 전에는 범용 registry, 빈 미래 클래스 또는 디렉터리를 만들지 않는다.
## 개발 및 검증 절차
1. 요구조건과 완료 기준을 먼저 문서화한다.
2. 정식화의 출처, 가정, 좌표계, 부호 및 적분 규칙을 기록한다.
3. 입력, semantic model 및 HDF5 계약을 구현 전에 확정한다.
4. 실패하는 단위·통합·reference 테스트와 모델을 먼저 작성한다.
5. 테스트를 통과하는 최소 코드를 구현한다.
6. 해석해, physics sanity 및 Abaqus 2024 골든 결과와 비교한다.
7. 물리량별 tolerance를 통과한 뒤에만 기능 완료와 내부 배포를 선언한다.
추가 규칙:
- 수직 파이프라인을 먼저 연결하되 임시 가짜 강성행렬은 사용하지 않는다.
- 같은 입력과 설정의 병렬 조립 결과는 재현 가능해야 한다.
- 전단강성이 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 Phase 1 기본값으로
적용한다.
- reference 비교는 metadata 없이 요청된 물리량과 CSV 경로를 명시한다. 현재
캔틸레버 샘플은 변위와 반력을 비교하며 요소 내력과 도심 응력 비교 루틴은
synthetic CSV로 검증한다.
- 새 MSVC 빌드 경고를 추가하지 않는다.
- 변경은 요청 범위에 한정하고 Conventional Commits 형식의 메시지를 사용한다.
## 검증 명령
Harness Python 검증:
```powershell
uv run --with pytest python -m pytest -v -rs
```
테스트가 0개 수집된 실행은 성공으로 인정하지 않는다.
Solver bootstrap 이후에는 `.harness/config.json`에 지정된 CMake Preset을 사용한다.
Preset이 없을 때만 다음 격리 build directory를 사용한다.
```powershell
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --output-on-failure
```
Harness phase 실행:
```powershell
python scripts/execute.py <phase-name>
```
사용자가 명시적으로 원격 push를 요청한 경우에만 다음을 사용한다.
```powershell
python scripts/execute.py <phase-name> --push
```
+277
View File
@@ -0,0 +1,277 @@
# FESA Architecture Decision Records
이 문서는 FESA의 주요 기술 선택과 포기한 대안을 기록한다. 현재 상태가 `Accepted`
결정은 Phase 1 계획과 구현에 적용한다. 결정을 변경할 때는 기존 기록을 지우지 않고
새 ADR에서 대체 관계를 명시한다.
## ADR-001: C++20, MSVC v143, x64와 CMake Presets
**상태:** Accepted
**상황:** 첫 배포는 Windows 개발팀 내부 검증용이며 Intel oneAPI와 HDF5를 일관되게
연동하고 Harness에서 자동 검증해야 한다.
**결정:** C++20, Visual Studio 2022 MSVC v143, Windows x64, CMake, CMake Presets,
CTest 및 GoogleTest/GoogleMock을 사용한다.
**결과와 트레이드오프:**
- `std::span` 등 C++20 기능으로 비소유 수치 view를 명시할 수 있다.
- CMake target 경계와 preset을 빌드 계약으로 사용할 수 있다.
- 다른 컴파일러, 운영체제 및 32비트 플랫폼은 Phase 1 보장 대상이 아니다.
## ADR-002: 외부 의존성은 개발 환경에 사전 설치
**상태:** Accepted
**상황:** oneMKL, oneTBB, HDF5와 GoogleTest의 공급 방식을 하나로 정해야 한다.
**결정:** 모든 외부 라이브러리는 개발·빌드 PC에 사전 설치하고 CMake가 설치 위치를
탐색한다. vcpkg나 Conan manifest는 Phase 1에 도입하지 않는다.
**결과와 트레이드오프:**
- 사내 표준 설치 환경을 그대로 사용할 수 있다.
- dependency bootstrap을 구현하지 않는다.
- 구성 단계는 누락, architecture 불일치 및 지원하지 않는 설치를 명시적으로
진단해야 한다.
- 재현성은 설치 버전 기록과 build environment 문서에 의존한다.
## ADR-003: 위험 우선 수직 파이프라인
**상태:** Accepted
**상황:** 첫 배포는 요소 종류보다 입력부터 결과까지의 코드 구조 검증에 초점을 둔다.
**결정:** 가장 작은 Beam 모델로 `.inp` 파싱, semantic model, DOF, 조립, constraint,
PARDISO, 결과 회복 및 HDF5 출력을 먼저 연결한다. 이후 합의된 입력 기능을 완성하고
마지막으로 요소 정확도 자격 검증을 수행한다.
**결과와 트레이드오프:**
- 모듈 계약과 데이터 누락을 일찍 발견한다.
- 임시 가짜 강성행렬은 사용하지 않고 실제 Timoshenko kernel의 최소 구현을 사용한다.
- 파이프라인 연결 완료는 수치적으로 검증된 배포를 뜻하지 않는다.
- Abaqus tolerance와 physics sanity를 통과해야 Phase 1 배포가 완료된다.
## ADR-004: Abaqus syntax와 solver semantic model 분리
**상태:** Superseded by ADR-013
**상황:** Abaqus `.inp` 부분집합을 지원하지만 내부 모델이 Abaqus 문법과 결합되면
해석 코드와 향후 입력 adapter가 오염된다.
**결정:** `io/abaqus`가 syntax를 파싱하고 검증된 `Domain` semantic model로
변환한다. 해석 계층에는 keyword 문자열, line layout 및 parser 임시 객체를 전달하지
않는다.
**결과와 트레이드오프:**
- 입력 adapter와 FEM 코어를 독립적으로 시험할 수 있다.
- syntax 오류와 semantic 오류를 분리할 수 있다.
- 이 결정의 syntax/semantic 분리 원칙은 유지되며 입력 조직 범위는 ADR-013이
대체한다.
## ADR-005: 2절점 3D Isoparametric Timoshenko Beam
**상태:** Accepted
**상황:** 첫 요소는 절점당 6자유도의 3D Beam이며 짧고 두꺼운 보의 전단변형을
표현해야 한다.
**결정:**
- 2절점 직선 Isoparametric Timoshenko Beam을 사용한다.
- 축·굽힘·비틀림 항은 2점, 전단 항은 1점 Gauss 적분한다.
- 일반 단면 \(A,I_y,I_z,J,A_{sy},A_{sz}\)와 등방성 선형 탄성을 사용한다.
- 도심·주축 단면, \(I_{yz}=0\), 단면 오프셋과 워핑 없음으로 제한한다.
**결과와 트레이드오프:**
- 전단변형을 표현하고 세장 보의 shear locking을 완화한다.
- reduced shear integration과 좌표변환을 별도로 검증해야 한다.
- 점별 전단·비틀림 응력은 단면 형상 정보가 없어 출력하지 않는다.
- 미래 Beam이나 shell formulation을 위한 범용 kernel framework를 미리 만들지 않는다.
## ADR-006: Essential BC 소거와 MKL PARDISO
**상태:** Accepted
**상황:** 최대 약 10만 자유도의 선형 정적 문제를 안정적으로 풀고 비영 지정값과
반력을 지원해야 한다.
**결정:** essential DOF를 소거해 reduced symmetric CSR system을 구성하고 MKL
PARDISO 대칭 양정치 직접해법으로 푼다. full solution을 복원한 뒤 원래 평형식에서
반력을 계산한다.
**결과와 트레이드오프:**
- 초기 구현과 singularity 진단이 반복해법보다 단순하고 안정적이다.
- PARDISO API와 handle은 `solvers/linear` adapter에 격리한다.
- MPC, penalty, Lagrange multiplier 및 iterative backend는 Phase 1에서 제외한다.
- 구속이 부족한 모델은 명시적 numerical failure로 처리한다.
## ADR-007: 결정적 oneTBB 요소 계산과 조립
**상태:** Accepted
**상황:** 요소 계산을 병렬화하면서 reference 회귀검증에 필요한 수치 재현성을
유지해야 한다.
**결정:** oneTBB로 요소별 contribution을 병렬 계산하고 thread-local 결과를 안정된
key로 정렬한 뒤 고정 순서로 합산해 CSR을 생성한다. PARDISO 실행 중에는 외부 TBB
작업을 중첩하지 않는다.
**결과와 트레이드오프:**
- thread scheduling 변화에 의한 비결정적 합산을 줄인다.
- 공유 CSR에 대한 원자적 무질서 누적을 피한다.
- 최대 throughput보다 재현성과 디버깅 가능성을 우선한다.
- 병렬화 이득이 작은 모델에는 scheduling overhead가 생길 수 있다.
## ADR-008: 자기완결형, 버전 지정 HDF5 결과
**상태:** Accepted
**상황:** 결과 파일만으로 모델과 해석 조건을 추적하고 reference comparison을
수행해야 한다.
**결정:** 모델, ID mapping, step, solver 설정, 절점·요소 결과와 diagnostic을 하나의
HDF5 파일에 저장하고 root에 schema version을 기록한다.
**결과와 트레이드오프:**
- 원본 `.inp` 없이도 결과 entity와 해석 조건을 추적할 수 있다.
- schema 변경을 명시적으로 versioning할 수 있다.
- 모델을 중복 저장하므로 결과 파일이 커진다.
- HDF5 writer/reader와 resource 수명 관리가 별도 adapter 책임이 된다.
## ADR-009: Abaqus 2024 오프라인 골든 검증
**상태:** Superseded by ADR-014 and ADR-015
**상황:** 상용 reference solver는 개발·CI 환경에서 자동 실행할 수 없지만 변위,
반력, 요소 내력 및 응력 비교가 필요하다.
**결정:** Abaqus/Standard 2024가 생성한 입력과 CSV 결과를 versioned golden data로
관리한다. 구체적인 CSV 선택과 전단 기본값 계약은 ADR-014와 ADR-015가 대체한다.
**결과와 트레이드오프:**
- CI에서 Abaqus 설치와 license가 필요하지 않다.
- 골든 데이터 갱신은 별도 Abaqus 환경과 수동 승인 절차가 필요하다.
- per-model metadata 요구사항은 ADR-015에서 제거한다.
## ADR-010: 검증 계층별 허용오차
**상태:** Accepted
**상황:** 모든 물리량에 하나의 상대오차를 적용하면 영에 가까운 값이나 서로 다른
규모의 결과를 올바르게 판정할 수 없다.
**결정:** 단위·정식화 테스트와 Abaqus 비교를 분리하고, reference 비교에는 기본
상대오차 \(10^{-5}\)와 물리량별 characteristic scale 기반 절대오차를 함께 사용한다.
**결과와 트레이드오프:**
- 영에 가까운 값과 큰 값 모두 의미 있게 비교할 수 있다.
- 모델별 예외 tolerance에는 문서화된 수치 근거가 필요하다.
- 단일 tolerance보다 comparison request와 helper가 복잡해진다.
## ADR-011: 일관 단위계와 결과 좌표계
**상태:** Accepted
**상황:** Abaqus와 같은 입력 호환성과 명확한 Beam 결과 부호를 유지해야 한다.
**결정:** FESA는 단위를 변환하지 않고 사용자가 일관 단위계를 제공한다. 절점
변위·회전과 반력은 전역좌표계로, 단면력·단면변형률과 회복응력은 요소
국부좌표계로 출력한다.
**결과와 트레이드오프:**
- 입력이 단순하고 Abaqus 모델과 공유하기 쉽다.
- 단위 일관성은 입력 작성자의 책임이다.
- HDF5에 요소별 국부 기저와 좌표계 metadata를 저장해야 한다.
## ADR-012: Phase 1 최소 실체화와 기존 Harness 유지
**상태:** Accepted
**상황:** 장기 아키텍처는 여러 요소와 해석 절차를 예상하지만, 첫 배포는 Beam
선형 정적 파이프라인에 한정된다. 저장소에는 이미 phase executor와 MSVC validation
hook이 있다.
**결정:**
- 필요한 모듈과 클래스만 해당 phase에서 만든다.
- 미래 taxonomy는 `docs/ARCHITECTURE.md`에 기록하되 빈 구현을 생성하지 않는다.
- 현재 `scripts/execute.py`, `docs/HARNESS.md`
`.agents/skills/harness/SKILL.md`의 실행 계약을 변경하지 않는다.
**결과와 트레이드오프:**
- 선행 abstraction과 사용되지 않는 상태를 줄인다.
- 두 번째 실제 요소나 analysis가 추가될 때 factory, registry 또는 state 계약을
확장한다.
- Harness phase는 현재의 `feat-{phase-name}` 브랜치, 재시도, guardrail 및
코드/metadata 분리 commit 동작을 따른다.
## ADR-013: 단일 Instance를 Domain으로 정규화
**상태:** Accepted
**상황:** 제공된 Abaqus 검증 모델은 Part/Assembly/Instance 구조를 사용하지만
해석 코어 전체에 Abaqus scope를 노출하면 Phase 1 복잡도가 크게 증가한다.
**결정:** flat/orphan mesh를 계속 지원하면서 여러 Part와 단일 Assembly·단일
무변환 Instance를 파싱한다. semantic mapper는 Instance가 참조하는 Part만 활성화해
flat `Domain`으로 정규화한다. 외부 entity는 `(instance name, part-local label)`
식별하고 dense solver index와 분리한다.
**결과와 트레이드오프:**
- 제공된 계층형 입력을 해석하면서 FEM·assembly·solver 경계를 유지한다.
- 사용되지 않는 Part는 파싱하되 해석 객체를 생성하지 않는다.
- Part와 Assembly 집합 scope를 별도로 해석해야 한다.
- 여러 Assembly/Instance, Instance 좌표변환 및 instance-local mesh 수정은 Phase 1
미지원 diagnostic이다.
## ADR-014: 생략된 Beam 전단강성의 Phase 1 기본값
**상태:** Accepted
**상황:** 일반 Beam 단면 입력과 제공된 검증 샘플에 명시적
`*TRANSVERSE SHEAR STIFFNESS`가 없지만 Timoshenko kernel에는 유효 전단면적이
필요하다.
**결정:** 전단강성이 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 적용한다.
지원되는 명시값은 기본값을 덮어쓰고 nonzero `SCF`는 거부한다.
**결과와 트레이드오프:**
- 현재 정사각형 캔틸레버 샘플의 전단 응답을 일관되게 표현할 수 있다.
- 이 값은 임의 일반 단면에 대한 보편적 Abaqus 기본값이 아니라 FESA Phase 1
가정이다.
- HDF5 결과에 전단 값과 입력/기본값 출처를 기록해야 한다.
## ADR-015: 명시적 물리량 선택 기반 CSV 검증
**상태:** Accepted
**상황:** 현재 캔틸레버 reference에는 변위와 반력만 있고 per-model metadata는
요구하지 않는다. 요소 내력과 응력 비교 기능은 해당 CSV가 추가되기 전에 구현해야
한다.
**결정:** comparison request가 물리량, CSV 경로, 상대 tolerance와 절대 scale을
명시한다. 현재 캔틸레버는 변위와 반력만 선택한다. 요소 내력은
`SF1..SF3/SM1..SM3`을 \(N,V_y,V_z,T,M_y,M_z\)로, 응력 `Sxx`는 요소 절점의
단면 도심 \(N/A\)로 비교한다. 요소 내력·응력 adapter는 synthetic CSV로 우선
검증한다.
**결과와 트레이드오프:**
- 누락된 비요청 CSV 때문에 현재 reference 검증이 차단되지 않는다.
- 요청한 파일이 없으면 실패하며 비요청 물리량을 통과로 오인하지 않는다.
- 단일 Instance에서는 Instance 열을 생략할 수 있다.
- tolerance와 검증 출처는 test registration과 `docs/VALIDATION.md`에서 관리한다.
+470
View File
@@ -0,0 +1,470 @@
# FESA Architecture
## 1. 목표
FESA의 아키텍처 목표는 Abaqus `.inp` 부분집합을 내부 semantic model로 변환하고,
유한요소 equation system을 구성해 구조해석 결과를 HDF5로 저장하며, reference
comparison과 physics sanity가 가능한 C++20/MSVC 솔버 구조를 제공하는 것이다.
핵심 품질 속성:
- FEM formulation traceability
- explicit I/O contracts
- sparse linear algebra backend isolation
- deterministic verification
- incremental feature addition
- Harness 기반 TDD와 workspace validation
## 2. 디렉터리 구조
public header와 implementation은 같은 모듈 구조를 사용한다.
```text
include/
fesa/
core/
io/
abaqus/
hdf5/
model/
fem/
elements/
materials/
assembly/
constraints/
solvers/
linear/
nonlinear/
analysis/
results/
validation/
src/
fesa/
core/
io/
abaqus/
hdf5/
model/
fem/
elements/
materials/
assembly/
constraints/
solvers/
linear/
nonlinear/
analysis/
results/
validation/
tests/
unit/
integration/
reference/
reference/
<model-id>/
model.inp
<model-id>_displacements.csv
<model-id>_reactions.csv
<model-id>_internalforces.csv
<model-id>_stresses.csv
.agents/
skills/
harness/
review/
.codex/
hooks.json
.harness/
config.example.json
config.json
docs/
scripts/
execute.py
hooks/
msvc_harness/
phases/
```
목표 구조는 장기적인 namespace와 책임 분류다. 실제 소스 디렉터리와 클래스는 해당
기능을 구현하는 phase에서만 만든다.
Phase 1에서 실체화하는 범위:
- `elements/beam`: 2절점 3D Timoshenko Beam
- `materials/elastic`: 등방성 선형 탄성
- `constraints`: essential BC elimination
- `solvers/linear`: MKL PARDISO
- `analysis`: `LinearStaticAnalysis`
- `results`: 단일 step/frame의 field와 diagnostic output
Truss, plane, solid, shell, plasticity, MPC, nonlinear, dynamic, frequency 및 heat
transfer는 목표 taxonomy로만 유지하고 빈 구현을 미리 만들지 않는다.
## 3. 모듈 경계
### `core`
ID, status, diagnostic, source location 및 작은 값 타입을 제공한다. 외부 라이브러리에
의존하지 않는다. 단위 변환 엔진은 두지 않고 일관 단위계 규약만 표현한다.
### `io/abaqus`
`.inp` lexer/parser, flat 또는 Part/Assembly/Instance scope record 및
syntax-to-semantic mapping을 담당한다. 해석 알고리즘과 equation numbering을 알지
않는다. parser의 임시 syntax 객체는 `model`에 노출하지 않는다. `*HEADING`,
`*PREPRINT`, `*RESTART`, `*OUTPUT`은 명시적인 no-op record로 처리하고 일반적인
unknown-keyword ignore 경로를 만들지 않는다.
### `io/hdf5`
HDF5 결과 writer/reader, schema versioning 및 HDF5 resource 수명을 담당한다.
HDF5 handle은 RAII wrapper 밖으로 노출하지 않는다.
### `model`
활성 Instance에서 정규화된 절점, 요소, 집합, 재료, 단면, step, 하중 및
경계조건의 solver semantic model을 소유한다. Part/Assembly keyword record나 MKL
자료구조를 저장하지 않는다.
### `fem`
DOF 정의, equation numbering 계약, quadrature, shape function, Jacobian 및
local/global mapping을 제공한다. 특정 analysis procedure에 종속되지 않는다.
### `elements`와 `materials`
요소의 local contribution과 결과 회복 계약을 제공한다. Phase 1 요소는 선형 문제에
필요한 local stiffness, equivalent load 및 section response만 계산한다.
### `assembly`
local-to-global mapping, sparse pattern 생성, contribution 정렬·병합 및 COO/CSR
변환을 담당한다. 요소 formulation이나 PARDISO handle을 소유하지 않는다.
### `constraints`
essential BC와 full/reduced vector 변환 정책을 담당한다. Phase 1에는 elimination만
구현하고 MPC, penalty 및 Lagrange multiplier는 추가하지 않는다.
### `solvers`
희소 선형계 backend 경계를 제공한다. Phase 1의 `solvers/linear`는 MKL PARDISO를
adapter로 감싸며 symbolic analysis, factorization, solve 및 release 수명을 관리한다.
### `analysis`
step data를 실행 가능한 `AnalysisModel`로 변환하고 DOF, assembly, constraint,
solver 및 result writer를 조율한다. 구체 수치 kernel이나 외부 API를 직접 구현하지
않는다.
### `results`
nodal, element, integration-point, field, history 및 diagnostic output의 semantic
표현을 담당한다. Phase 1에는 nodal/element field와 diagnostic만 실체화한다.
### `validation`
reference mapping, 비교 metric, tolerance와 physics sanity helper를 제공한다.
production parser와 solver 내부 상태를 우회하는 별도 해석 경로를 만들지 않는다.
## 4. 핵심 객체 모델
```text
ParsedDeck (io/abaqus 전용)
├── PartDefinition[]
├── AssemblyDefinition
│ └── InstanceDefinition[1]
├── MaterialDefinition[]
└── StepDefinition
Domain
├── Node
├── Element
├── Material
├── Property
├── NodeSet
├── ElementSet
├── BoundaryCondition
├── Load
└── StepDefinition
AnalysisModel
├── active elements
├── active loads
├── active boundary conditions
├── active properties/materials
└── equation system view
AnalysisState
├── displacement U
├── external force Fext
├── internal force Fint
├── residual R
├── reaction
└── element/integration-point response
DofManager
├── node dof definitions
├── constrained/free dof mapping
├── equation numbering
├── element equation adjacency view
└── full/reduced vector reconstruction
Results
└── ResultStep
└── ResultFrame
├── FieldOutput
├── HistoryOutput
└── DiagnosticOutput
```
장기 목표인 `AnalysisState`의 velocity, acceleration, temperature, increment,
iteration 및 material state는 해당 해석 기능을 구현할 때 추가한다. Phase 1 객체에
사용되지 않는 상태를 미리 할당하지 않는다.
## 5. 상태 관리
- `Domain`은 입력에서 만들어진 전체 모델 정의를 소유한다.
- syntax-to-semantic mapping과 validation이 끝난 `Domain`은 가능한 한 불변으로
취급한다.
- 계층형 입력의 외부 ID는 `(instance name, part-local label)`로 표현하고 내부
dense index와 구분한다. flat 입력은 예약된 global scope를 사용한다.
- 여러 Part를 파싱할 수 있지만 단일 Assembly의 단일 무변환 Instance가 참조하는
Part만 `Domain`에 포함한다.
- `AnalysisModel`은 현재 step에서 활성화되는 객체의 ID/참조 기반 view다. `Domain`
복제하지 않는다.
- `DofManager`는 DOF와 equation numbering을 전담한다. `Node``Element`
equation ID를 저장하지 않는다.
- `AnalysisState`는 해석 중 변하는 물리량만 소유한다.
- 결과는 `ResultStep -> ResultFrame -> FieldOutput/HistoryOutput` 구조로 관리한다.
## 6. 데이터 흐름
```text
Abaqus input file
-> lexer/parser
-> scoped syntax records
-> complete-deck reference resolution
-> flat scope 또는 단일 active Part/Instance 선택
-> set/material/section/load/BC 정규화와 validation
-> immutable Domain
-> StepDefinition
-> AnalysisModel
-> DofManager
-> sparse pattern
-> element contributions
-> deterministic Assembler
-> essential BC elimination
-> MKL PARDISO
-> full state/reaction reconstruction
-> element result recovery
-> Results
-> HDF5 writer
```
파싱, semantic validation, equation construction, solve 및 output 단계는 서로 다른
diagnostic context를 유지한다.
## 7. 해석 실행 흐름
`Analysis::run()`은 다음 생명주기를 고정한다.
```text
initialize
buildAnalysisModel
buildDofMap
buildSparsePattern
executeProcedure
finalizeResults
```
Phase 1의 `LinearStaticAnalysis::executeProcedure()`는 다음을 수행한다.
```text
assemble
applyBoundaryConditions
solve
reconstructFullState
recoverReactions
recoverElementResults
writeResults
```
미래의 비선형·동적 해석은 `executeProcedure` 내부에 각각 Newton 또는 time-step
loop를 소유한다. base class가 모든 해석 종류의 반복 변수를 미리 소유하지 않는다.
## 8. Timoshenko Beam kernel
Phase 1 kernel은 다음 입력만 받는다.
- 두 절점 좌표
- 12개 local/global DOF mapping
- \(E,\nu\)
- \(A,I_y,I_z,J,A_{sy},A_{sz}\)
- 국부 단면축 기준 방향
- 필요한 평가 위치와 회복점
kernel 책임:
- 강건한 국부 직교 기저 생성
- 자연좌표 shape function과 Jacobian 평가
- 축·굽힘·비틀림 2점 Gauss 적분
- 전단 1점 Gauss 적분
- local stiffness와 global transformation
- section strain/resultant와 \(\sigma_{xx}\) 회복
kernel은 Abaqus의 slenderness compensation을 구현하지 않는다. 명시적 전단강성이
없으면 semantic mapper가 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 적용한다. 명시적
전단강성은 이 기본값을 덮어쓰며 nonzero `SCF`는 거부한다.
## 9. 희소 조립과 병렬성
1. `assembly`의 sparse pattern builder가 요소 connectivity와 `DofManager`
equation mapping으로 sparsity pattern을 생성한다.
2. oneTBB가 요소별 local contribution을 독립적으로 계산한다.
3. worker는 공유 CSR 값 배열에 무질서하게 누적하지 않고 thread-local contribution을
생성한다.
4. contribution을 전역 row, column 및 안정된 tie-break key로 정렬한다.
5. 고정된 순서로 합산해 대칭 CSR을 생성한다.
6. essential BC를 소거해 reduced symmetric system을 만든다.
7. TBB 작업이 끝난 뒤 MKL PARDISO를 호출한다.
성능보다 같은 입력·설정에서의 수치 재현성을 우선한다. 병렬·직렬 결과 비교와 thread
count 변화 테스트를 reference suite에 포함한다.
## 10. 선형해법 backend
`LinearSolver` 경계는 matrix structure, numeric values, RHS를 입력받고 solution과
진단을 반환한다. Phase 1의 유일한 구현은 `PardisoLinearSolver`다.
PARDISO adapter 책임:
- 0-based 대칭 CSR 계약 검증
- analysis, factorization, solve 및 release phase 관리
- MKL error code를 FESA diagnostic으로 변환
- matrix checker와 residual diagnostic 제공
- handle과 workspace의 RAII 수명 관리
반력은 reduced solve 결과를 full vector로 복원한 뒤 원래 시스템의
\(r=Ku-f\)에서 계산한다.
## 11. HDF5 schema
최상위 구조:
```text
/
├── metadata
├── model
│ ├── nodes
│ ├── elements
│ ├── sets
│ ├── materials
│ ├── properties
│ └── id_maps
├── analysis
│ ├── steps
│ ├── boundary_conditions
│ ├── loads
│ └── solver_settings
├── results
│ └── steps/<step-id>/frames/<frame-id>
│ ├── nodal
│ ├── element
│ └── history
└── diagnostics
```
작은 schema 정보와 설명은 attribute로, 수치 배열과 가변 크기 데이터는 dataset으로
저장한다. root metadata에는 schema version, FESA version, 입력 fingerprint,
좌표계 및 단위 정책을 기록한다. 계층형 입력은 Part/Instance 이름, part-local ID와
전단강성 값의 입력/기본값 출처를 함께 저장한다.
## 12. 오류 처리
진단은 최소한 다음 분류를 가진다.
- I/O 및 encoding 오류
- lexical/syntax 오류
- 미지원 keyword/option
- semantic reference 오류
- model validity 오류
- equation system 오류
- numerical solver 오류
- result recovery 또는 HDF5 오류
각 진단은 가능한 경우 source file, line, keyword, entity ID, analysis stage 및 원인을
포함한다.
다음 조건은 묵시적으로 보정하지 않고 실패시킨다.
- 길이가 0인 요소
- 요소축과 평행하거나 길이가 0인 단면 방향 벡터
- 존재하지 않는 절점·집합·재료·단면 참조
- 중첩 집합 순환
- 여러 Assembly/Instance 또는 Instance 평행이동·회전
- Instance가 참조하지 않는 Part entity를 Assembly 집합이 참조하는 경우
- 재료나 단면이 없거나 중복 할당된 요소
- 상충하는 경계조건
- 강체모드가 남은 singular equation system
- NaN 또는 무한대 입력·결과
## 13. 설계 패턴
- Adapter: Abaqus, MKL, TBB 및 HDF5 경계
- RAII: PARDISO handle, HDF5 object와 temporary workspace
- Strategy: 실제 교체 가능성이 있는 solver와 writer 경계
- Template Method: `Analysis::run()`의 공통 생명주기
- Factory: Phase 1에는 B31을 생성하는 명시적 factory
- Registry: 두 번째 실제 요소나 material type이 추가되는 phase에서만 도입
- Runtime polymorphism: assembly가 구체 요소 내부 상태를 알지 않게 하는 최소 계약
대규모 모델에서 virtual dispatch가 병목이라는 측정 결과가 있을 때만 타입별 batch
kernel을 추가한다.
## 14. 검증 구조
- `tests/unit`: 값 타입, parser 단위, shape function, quadrature, transformation,
element matrix
- `tests/integration`: `.inp`에서 HDF5까지 전체 경로
- `tests/reference`: CSV 골든 결과와 FESA HDF5 결과 비교
- `reference/<model-id>`: Abaqus 입력과 현재 사용할 수 있는 결과 CSV
reference comparison request가 비교할 물리량과 CSV 경로, 상대 tolerance 및
물리량별 절대 scale을 명시한다. 요청한 CSV가 없으면 실패하며 요청하지 않은 결과를
통과로 표시하지 않는다. 현재 캔틸레버는 변위와 반력만 요청하고, 요소 내력과
단면 도심 응력 adapter는 synthetic CSV로 검증한다.
요소 내력 CSV의 `(Instance, Element Label, Node Label)` 위치에서
`SF1,SF2,SF3,SM1,SM2,SM3`을 \(N,V_y,V_z,T,M_y,M_z\)로 매핑한다. 응력 CSV의
같은 위치에 있는 `Sxx`는 단면 도심값 \(N/A\)와 비교한다. 단일 Instance에서는
Instance 열 생략을 허용하되 comparison request가 제공한 Instance 이름으로
보완한다.
reference helper는 반드시 public parser와 analysis 경로로 FESA 결과를 생성한다.
테스트 전용 경로로 Domain이나 matrix를 직접 주입해 전체 파이프라인 결함을 숨기지
않는다.
## 15. Harness 실행 계층
현재 저장소의 `scripts/execute.py`, `docs/HARNESS.md`
`.agents/skills/harness/SKILL.md`를 실행 계약으로 사용한다.
Executor 동작:
- `feat-{phase-name}` 브랜치 생성 또는 checkout
- `AGENTS.md``docs/*.md` guardrail 주입
- 완료된 step의 `summary`를 다음 prompt에 전달
- 실패 시 이전 오류를 포함해 최대 3회 재시도
- 코드 변경과 phase metadata를 분리해 commit
- step/phase timestamp 기록
- `--push` 사용 시에만 원격 push
Codex hook 동작:
- PreToolUse hook은 위험한 명령 패턴을 검사한다.
- Stop hook은 감지된 C/C++ 프로젝트를 MSVC로 빌드하고 테스트한다.
- `.harness/config.json`이 있으면 해당 설정과 preset을 우선한다.
현재 executor에 없는 `allowed_paths`, `validate_workspace.py`,
`codex/<phase-name>` 브랜치 및 명시적 clean-worktree 정책은 FESA 아키텍처의
요구사항으로 간주하지 않는다.
+117
View File
@@ -0,0 +1,117 @@
# 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++ 저장소는
오류로 처리한다.
설정을 시작하려면 다음을 실행한다.
```powershell
Copy-Item .harness/config.example.json .harness/config.json
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 --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는
별도의 로그 파일을 만들지 않는다.
+240
View File
@@ -0,0 +1,240 @@
# PRD: FESA
## 1. 제품 목표
FESA는 Abaqus `.inp` 제한 부분집합으로 정의된 유한요소 모델을 읽고 선형 정적
구조해석을 수행한 뒤, 모델과 절점·요소 결과를 자기완결형 HDF5 파일로 저장하는
C++20/MSVC 기반 내부 검증용 솔버다.
첫 배포는 Abaqus나 Nastran의 기능 범위를 재현하는 것이 아니라 다음 기반을 검증하는
데 목적이 있다.
- 입력에서 결과까지 이어지는 전체 해석 파이프라인
- FEM 정식화를 추적할 수 있는 모듈 구조
- 명시적인 입력·내부 모델·출력 계약
- MKL, TBB 및 HDF5를 격리하는 backend 경계
- 해석해, physics sanity 및 reference 비교가 가능한 TDD 구조
## 2. 대상 사용자와 배포 형태
- 주 사용자: FESA를 개발하고 검증하는 1인 개발자
- 배포 대상: 개발팀 내부 검증 환경
- 산출물: 정적 해석 코어 라이브러리, CLI, 예제 입력, HDF5 schema 문서,
reference 데이터 및 검증 보고서
- 일정: 고정 기한보다 단계별 완료 조건과 품질 게이트를 우선한다.
## 3. Phase 1 기능 범위
### 3.1 해석
- 소변형 선형 정적 해석
- 단일 `*STEP`과 단일 `*STATIC` 하중 케이스
- 약 10만 자유도 이하
- 비영 지정 변위·회전을 포함한 essential boundary condition
- 일관 단위계 사용; FESA 내부 단위 변환 없음
### 3.2 요소와 정식화
- 2절점 직선 3D Isoparametric Timoshenko Beam
- 절점당 자유도:
\(u_x,u_y,u_z,\theta_x,\theta_y,\theta_z\)
- 선형 형상함수와 자연좌표 \(\xi\in[-1,1]\)
- 선택적 감차적분:
- 축·굽힘·비틀림 항: 2점 Gauss 적분
- 전단 항: 1점 Gauss 적분
- 등방성 선형 탄성:
- 입력: \(E,\nu\)
- 계산: \(G=E/[2(1+\nu)]\)
- 일반 단면 semantic property:
\(A,I_y,I_z,J,A_{sy},A_{sz}\)
- 도심과 전단중심이 일치하는 주축 단면
- \(I_{yz}=0\), 단면 오프셋과 워핑 없음
- 요소축과 평행하지 않은 국부 단면 기준 방향 벡터 필수
- 여러 재료와 여러 단면을 `ELSET`별로 할당
### 3.3 Abaqus 입력 부분집합
입력은 다음 두 조직 중 하나를 사용한다.
- 전역 절점·요소로 구성된 flat/orphan mesh
- 여러 Part 정의와 좌표변환이 없는 단일 Assembly·단일 Instance
계층형 입력에서는 Assembly의 Instance가 참조하는 Part만 해석에 사용한다. 사용되지
않는 Part는 파싱하지만 해석 `Domain`에 포함하지 않는다. 외부 entity는
`(instance name, part-local label)`로 식별하고 내부 dense index와 분리한다.
필수 지원 대상:
- `*NODE`
- `*ELEMENT, TYPE=B31`
- `*PART`, `*END PART`
- `*ASSEMBLY`, `*END ASSEMBLY`
- `*INSTANCE`, `*END INSTANCE`
- `*NSET`, `*ELSET`
- 명시적 ID 목록
- `GENERATE`
- 기존 집합을 참조하는 중첩 집합
- `*MATERIAL`, `*ELASTIC`
- `*BEAM GENERAL SECTION, SECTION=GENERAL`
- `*TRANSVERSE SHEAR STIFFNESS`(선택)
- `*BOUNDARY`
- `*CLOAD`
- `*STEP`, `*STATIC`, `*END STEP`
계층형 입력은 좌표변환이 없는 단일 Instance만 허용한다. 여러 Assembly/Instance,
Instance 평행이동·회전, instance-local mesh 수정 및 flat/계층 mesh 혼합은 파일
위치와 원인을 포함한 diagnostic으로 거부한다. Part 집합과 Assembly 집합은 scope를
구분하며, Assembly의 `INSTANCE=` 집합을 활성 Part의 로컬 ID에 연결한다.
파서는 Abaqus syntax를 semantic model로 변환한다. `*HEADING`, `*PREPRINT`,
`*RESTART`, `*OUTPUT`은 명시적으로 지원하는 no-op directive로 처리한다. 그 밖의
지원하지 않는 keyword나 option을 묵시적으로 무시하지 않는다.
명시적 전단강성이 있으면 FESA의 \(A_{sy},A_{sz}\)를 재료의 \(G\)와 일관되게
구성한다. 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 Phase 1 기본값으로
적용한다. 명시된 `SCF`가 0이 아니면 미지원 입력으로 거부한다.
### 3.4 하중과 경계조건
- `*BOUNDARY`: 6개 절점 자유도의 0 또는 비영 지정값
- `*CLOAD`: 절점 집중력과 집중모멘트
- 지정값이 중복되거나 충돌하면 semantic validation 오류
- 분포하중, 중력, 압력 및 follower load는 제외
### 3.5 결과
절점 결과는 전역좌표계로 출력한다.
- 변위와 회전
- 반력과 반력모멘트
요소 결과는 요소 국부좌표계로 출력한다.
- 단면력 \(N,V_y,V_z,T,M_y,M_z\)
- 대응 단면변형률
- 사용자 지정 단면 회복점 \((y,z)\)에서 축력과 이축 굽힘에 의한
\(\sigma_{xx}\)
- 요소별 국부 기저 벡터
- 계층형 입력의 Part/Instance 이름과 part-local ID
점별 전단응력과 비틀림응력은 단면 형상 정보 없이는 유일하게 복원할 수 없으므로
Phase 1에서 출력하지 않는다.
HDF5 결과는 다음 정보를 함께 갖는 자기완결형 파일이어야 한다.
- schema와 FESA 버전
- 원본 입력 식별 정보
- 절점, 요소, 집합, 재료 및 단면
- 외부 ID와 내부 dense index mapping
- step과 solver 설정
- 적용된 전단강성과 입력값/기본값 출처
- 절점·요소 결과
- 수렴·평형·solver diagnostic
## 4. 수치해법과 병렬화 요구사항
- 지정 자유도 소거 후 reduced equation system을 구성한다.
- 소거 전 평형식 \(r=Ku-f\)를 이용해 반력을 복원한다.
- 전역 강성행렬은 대칭 CSR로 저장한다.
- MKL PARDISO 대칭 양정치 직접해법을 기본 backend로 사용한다.
- oneTBB는 요소 강성·하중·결과 계산과 조립 전처리에 사용한다.
- 선형해법 실행 중에는 외부 TBB 작업을 중첩하지 않고 MKL 내부 병렬화를 사용한다.
- 부동소수점 contribution의 병합 순서를 고정해 같은 설정에서 재현 가능한 결과를 낸다.
## 5. 검증 요구사항
### 5.1 검증 계층
1. 단위 테스트
- 형상함수 partition of unity
- Jacobian과 Gauss 적분
- 국부 기저 직교성
- 좌표변환
- 요소 강성 대칭성
2. 정식화 테스트
- 강체운동에서 무변형
- 축력, 비틀림, 단축 및 이축 굽힘
- 전단 지배 문제
- 세장비 변화와 shear locking
3. 통합 테스트
- 입력 파싱부터 HDF5 출력까지 전체 파이프라인
- 평형 \(Ku-f-r\)
- 비영 지정 변위
- 여러 재료·단면과 중첩 집합
4. Reference 테스트
- Abaqus/Standard 2024 B31 결과
- 현재 캔틸레버의 변위와 반력
- 요소 내력 및 요소 절점 단면 도심 응력 비교 계약의 synthetic CSV 검증
### 5.2 골든 데이터
Abaqus는 CI나 Harness에서 자동 실행하지 않는다. 별도 Abaqus 2024 환경에서 수동으로
생성한 입력과 CSV 결과를 `reference/<model-id>/`에 보관하며 per-model metadata
파일은 요구하지 않는다.
비교 실행은 물리량과 해당 CSV 경로를 명시한다. 요청한 파일이 없으면 실패하고,
요청하지 않은 물리량은 통과로 보고하지 않는다. 현재 `reference/cantilever beam`
샘플은 변위와 반력만 비교한다. 요소 내력과 응력 CSV가 추가되기 전까지 해당
reader와 비교 kernel은 synthetic CSV로 검증한다.
CSV 식별 및 값 열:
- 변위: `Part Instance Name`, `Node Label`, `U-U1..U-U3`, `UR-UR1..UR-UR3`
- 반력: `Part Instance Name`, `Node Label`, `RF-RF1..RF-RF3`, `RM-RM1..RM-RM3`
- 요소 내력: `Part Instance Name`, `Element Label`, `Node Label`,
`SF-SF1..SF-SF3`, `SM-SM1..SM-SM3`
- 요소 응력: `Part Instance Name`, `Element Label`, `Node Label`, `Sxx`
단일 Instance에서는 `Part Instance Name` 열을 생략할 수 있다. 내력은
`SF1,SF2,SF3,SM1,SM2,SM3`을 각각 \(N,V_y,V_z,T,M_y,M_z\)로 비교한다.
응력은 요소 절점의 단면 도심값 \(\sigma_{xx}=N/A\)를 비교한다.
### 5.3 허용오차
- 단위·정식화 테스트는 정규화된 엄격한 tolerance를 사용한다.
- Abaqus 비교 기본 상대오차는 \(10^{-5}\)로 한다.
- 영에 가까운 결과는 특성 길이, 하중 및 응력에 기반한 절대오차를 함께 사용한다.
- formulation 또는 output 위치 차이로 별도 tolerance가 필요하면 comparison
test 설정과 `docs/VALIDATION.md`에 근거를 기록한다.
## 6. 개발 워크플로우
각 기능은 다음 게이트를 순서대로 통과한다.
1. 요구조건과 완료 기준 정의
2. 책, 논문 및 공식 문서 조사
3. FEM 정식화, 가정, 좌표계, 부호 및 적분 규칙 작성
4. 입력·semantic model·HDF5 데이터 계약 정의
5. 실패하는 테스트와 개발 솔버/Abaqus 모델 작성
6. 테스트를 통과하는 최소 코드 구현
7. 현재 Abaqus 변위·반력 비교와 요소 내력·응력 비교 루틴 검증
8. tolerance 및 physics sanity 통과
9. 내부 배포
파이프라인 수직 슬라이스를 먼저 완성하지만, 이는 수치적으로 자격이 검증된 배포를
의미하지 않는다. 요소 정확도와 Abaqus 비교 게이트까지 통과해야 Phase 1이 완료된다.
## 7. 제외 범위
- Truss, bar, plane, solid, shell 및 다른 Beam 요소
- 여러 step과 하중 이력
- 여러 Assembly/Instance, Instance 평행이동·회전 및 instance-local mesh 수정
- 분포하중, 압력, 중력 및 체적력
- MPC, RBE2, RBE3, penalty 및 Lagrange multiplier constraint
- 기하·재료 비선형, 접촉, 좌굴, 모달, 동적 및 열전달
- 소성, 직교이방성 및 사용자 재료
- 단면 오프셋, 곱관성모멘트, 전단중심 편심 및 워핑
- 점별 전단·비틀림 응력
- 단위 변환
- 외부 고객용 installer와 API 호환성 보장
## 8. 내부 배포 수용 조건
- MSVC Debug와 Release 구성에서 새 경고 없이 빌드
- 모든 GoogleTest/CTest와 Harness 검증 통과
- 테스트 0개 수집이 아님을 확인
- 전체 입력-해석-출력 통합 테스트 통과
- physics sanity와 평형 잔차 기준 통과
- 현재 Abaqus 2024 변위·반력 골든 결과의 tolerance 통과
- 요소 내력·도심 응력 CSV adapter와 비교 kernel의 synthetic 검증 통과
- HDF5 schema, 입력 부분집합, 정식화 및 검증 보고서 제공
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,312 @@
# FESA Phase 1 Abaqus Assembly and Reference Comparison Design
## 1. Status and purpose
This document records the approved Phase 1 design changes for:
- parsing both flat/orphan-mesh and Part/Assembly/Instance Abaqus inputs;
- activating only the Part referenced by one untransformed Instance;
- applying Phase 1 transverse-shear defaults when the input omits them; and
- validating displacement, reaction, element internal force, and centroidal
element stress without per-model metadata files.
It refines the existing Phase 1 product and architecture documents. It does not
authorize solver implementation.
## 2. Phase 1 input scope
FESA supports two mutually exclusive input organizations.
### 2.1 Flat/orphan mesh
Nodes, elements, sets, sections, boundary conditions, and loads are defined in
the global input scope. These records map directly to the solver `Domain`.
### 2.2 Part/Assembly/Instance
The parser preserves the following syntax scopes:
```text
ParsedDeck
├── PartDefinition[]
├── AssemblyDefinition
│ └── InstanceDefinition[1]
├── MaterialDefinition[]
└── StepDefinition
```
Phase 1 accepts exactly one Assembly containing exactly one Instance. The
Instance must:
- reference one defined Part;
- contain no translation or rotation data; and
- contain no instance-local mesh modifications.
Multiple Part definitions may be parsed, but only the Part referenced by the
accepted Instance is expanded into the analysis `Domain`. Unreferenced Parts
remain syntax/semantic input records and do not contribute nodes, elements,
properties, loads, degrees of freedom, or results.
Multiple Instances, multiple Assemblies, mixed flat and hierarchical meshes,
missing Part references, and Instance transformation data are semantic errors.
Diagnostics identify the keyword and source location that caused the failure.
The following non-analysis directives found in the supplied Abaqus sample are
explicitly recognized no-op records rather than silently ignored unknown
keywords:
- `*HEADING`
- `*PREPRINT`
- `*RESTART`
- `*OUTPUT`
Comments and output-request data owned by these directives do not affect the
analysis `Domain`. Any other unsupported keyword or option remains an error.
## 3. Scope and identity resolution
Part-local node and element labels may be reused by future Instances. The
semantic layer therefore identifies instantiated entities by:
```text
(instance_name, part_local_label)
```
The Phase 1 single-Instance restriction means this composite key is not needed
to disambiguate the current equation system, but it is retained as explicit
input and result provenance. Dense internal indices remain separate from
external Abaqus labels.
For a hierarchical input:
- Part `NSET` and `ELSET` definitions resolve in Part scope.
- Material and section references are resolved after the complete deck has
been parsed, so their textual declaration order does not control validity.
- Section assignments made to a Part `ELSET` are applied to the active
Instance.
- Assembly `NSET` and `ELSET` definitions with `INSTANCE=` lift local labels
from the active Part into Assembly scope.
- Nested set references resolve within their declared scope with cycle
detection and deterministic sorted-unique membership.
- Step-level `*BOUNDARY` and `*CLOAD` references resolve through Assembly sets.
For a flat input, the same semantic contracts use a reserved global scope and
do not require an Instance name.
The normalized `Domain` contains analysis entities and their provenance, but it
does not expose Abaqus keyword records to FEM, element, assembly, constraint, or
solver modules.
## 4. Normalization flow
```text
Abaqus input
-> scoped syntax records
-> complete-deck name and reference resolution
-> organization validation
-> active Part/Instance selection or flat-scope selection
-> set, material, section, load, and boundary resolution
-> normalized immutable Domain
-> existing analysis pipeline
```
The normalization stage is the only production layer that understands both
Abaqus scopes and the flat solver `Domain`. The analysis pipeline is not made
hierarchy-aware in Phase 1.
## 5. Transverse-shear default
If `*TRANSVERSE SHEAR STIFFNESS` is absent, the Phase 1 semantic mapper applies:
\[
A_{sy}=A_{sz}=\frac{5}{6}A
\]
and uses `SCF=0`.
If explicit transverse-shear stiffness is present, its supported values
override the effective shear-area default. An explicitly specified nonzero
`SCF` is unsupported in Phase 1 and produces an input diagnostic.
The result database records whether the effective shear properties came from
the input or the Phase 1 default. This rule is a documented FESA Phase 1
assumption; it is not presented as a general default for arbitrary Abaqus
sections.
## 6. Reference comparison contract
No `metadata.json` file is required. A comparison request explicitly supplies:
- the result quantities to compare;
- the path of each requested reference CSV;
- relative tolerance; and
- quantity-specific absolute scale.
The default relative tolerance is \(10^{-5}\). Absolute scales are defined by
the PRD and test registration rather than by per-model metadata.
The comparison supports four quantities:
1. nodal displacement and rotation;
2. nodal reaction force and moment;
3. element-node section force and moment; and
4. element-node centroidal axial stress.
A requested missing file is an error. A quantity not selected by the comparison
request is not required and is not silently reported as passed.
The supplied `reference/cantilever beam` test initially requests only
displacement and reaction. The element-force and stress readers and comparison
kernels are still implemented and tested with synthetic reference tables.
When the corresponding Abaqus CSV files are added, the same reference test can
select all four quantities without changing the comparison kernel.
## 7. Reference CSV schemas
CSV readers trim surrounding whitespace from headers and values. A UTF-8 byte
order mark on the first header is tolerated. Entity rows must be unique for the
key required by their result type.
### 7.1 Displacement
The existing Abaqus field-report columns are accepted:
```text
Part Instance Name, Node Label,
U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3
```
For a single Instance, `Part Instance Name` may be omitted.
### 7.2 Reaction
The existing Abaqus field-report columns are accepted:
```text
Part Instance Name, Node Label,
RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3
```
For a single Instance, `Part Instance Name` may be omitted.
### 7.3 Element internal force
```text
Part Instance Name, Element Label, Node Label,
SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3
```
The canonical component mapping is:
```text
SF1 -> N
SF2 -> Vy
SF3 -> Vz
SM1 -> T
SM2 -> My
SM3 -> Mz
```
The element label and element-end node label together identify the result
position. For a single Instance, `Part Instance Name` may be omitted.
### 7.4 Element stress
```text
Part Instance Name, Element Label, Node Label, Sxx
```
`Sxx` is compared with the FESA element-end stress at the section centroid:
\[
\sigma_{xx,\mathrm{centroid}}=\frac{N}{A}
\]
The reference comparison does not use bending stress at an unspecified
recovery point. FESA may still store explicitly identified non-centroid
recovery-point stresses in its HDF5 result contract. For a single Instance,
`Part Instance Name` may be omitted.
## 8. Comparison behavior
Each scalar component uses the normalized error:
\[
e_n=\frac{|a-r|}{a_\mathrm{scale}+r_\mathrm{tol}|r|}
\]
and passes when \(e_n\leq1\).
Before evaluation, the comparison layer rejects:
- nonfinite actual or reference values;
- duplicate entity/result-position rows;
- unknown Instance, node, or element labels;
- an element-node pair that is not part of the referenced element;
- missing requested components; and
- reference rows that cannot be matched to one FESA result.
The comparison report identifies the quantity, entity key, component,
reference value, actual value, tolerance, and normalized error for every
failure.
## 9. Required tests
### 9.1 Parser and semantic normalization
- A flat deck still produces a valid `Domain`.
- The supplied hierarchical cantilever deck produces an active Domain from its
single referenced Part.
- Unreferenced Parts do not contribute analysis entities.
- Part and Assembly sets resolve in the correct scope.
- Material definitions declared after the Part resolve correctly.
- Multiple Instances and multiple Assemblies are rejected.
- Translation and rotation data are rejected.
- Missing Part references and wrong Instance names are rejected.
- Unsupported keywords are not silently ignored.
- Omitted transverse-shear data yields \(A_{sy}=A_{sz}=5A/6\) and `SCF=0`.
### 9.2 Reference adapters and comparison
- Existing displacement and reaction CSV files parse after whitespace
normalization.
- Optional single-Instance columns are handled without weakening multi-scope
identity checks.
- Synthetic internal-force rows verify all six component mappings.
- Synthetic stress rows verify centroidal \(N/A\) comparison.
- Requested missing files, duplicates, invalid IDs, invalid element-node pairs,
missing columns, and nonfinite values fail.
- Relative and absolute tolerance behavior is tested near zero and at
representative scales.
- The current cantilever integration test selects displacement and reaction
only.
## 10. Phase-plan impact
The existing ten Harness phases remain. Four phases receive revised steps:
- `domain-and-input-skeleton`: add scoped Part/Assembly/single-Instance syntax
records and minimal normalization.
- `abaqus-subset-completion`: complete scoped set resolution, active-Part
expansion, no-op directive handling, and transverse-shear defaults.
- `result-contract-completion`: recover and expose element-end internal force
and centroidal `Sxx` alongside the complete result contract.
- `beam-reference-qualification`: replace metadata-driven discovery with
explicit comparison requests and use the supplied cantilever displacement
and reaction files as the initial real reference.
Phase step files must keep parser syntax handling, semantic normalization,
result recovery, CSV adaptation, and numeric comparison in separate
module-bounded steps.
## 11. Completion criteria for this design change
Planning is consistent when:
- PRD, architecture, ADR, detailed implementation plan, and Harness step draft
describe the same single-Instance scope;
- no planning artifact requires reference metadata;
- the current cantilever reference requires only displacement and reaction;
- element internal-force and centroidal-stress comparison remain mandatory
implementation work; and
- no document claims support for multiple or transformed Instances in Phase 1.
@@ -0,0 +1,31 @@
{
"project": "FESA",
"phase": "abaqus-subset-completion",
"steps": [
{
"step": 0,
"name": "abaqus-input-contract",
"status": "pending"
},
{
"step": 1,
"name": "part-and-assembly-set-resolution",
"status": "pending"
},
{
"step": 2,
"name": "single-instance-semantic-validation",
"status": "pending"
},
{
"step": 3,
"name": "material-section-and-shear-defaults",
"status": "pending"
},
{
"step": 4,
"name": "step-bc-load-and-noop-directives",
"status": "pending"
}
]
}
+52
View File
@@ -0,0 +1,52 @@
# Step 0: Abaqus Input Contract
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/include/fesa/io/abaqus/`
- `/reference/cantilever beam/cantilever beam.inp`
## 작업
production parser를 수정하기 전에 `docs/ABAQUS_INPUT_SUBSET.md`를 normative contract로
작성하고 valid/invalid fixture matrix를 등록한다.
문서에 각 keyword의 허용 scope, parameter, data line, diagnostic을 정확히 정의한다.
- `*NODE`, `*ELEMENT,TYPE=B31`
- `*PART/*END PART`, `*ASSEMBLY/*END ASSEMBLY`,
`*INSTANCE/*END INSTANCE`
- `*NSET`, `*ELSET`, `GENERATE`, nested set, `INSTANCE=`
- `*MATERIAL`, `*ELASTIC`, `*BEAM GENERAL SECTION`
- optional `*TRANSVERSE SHEAR STIFFNESS`
- `*BOUNDARY`, `*CLOAD`, `*STEP`, `*STATIC`, `*END STEP`
- no-op `*HEADING`, `*PREPRINT`, `*RESTART`, `*OUTPUT`
`tests/fixtures/abaqus/valid``invalid`에 최소 한 규칙당 fixture를 정의하고
data-driven contract test를 먼저 실패시킨다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R AbaqusInputContract --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 문서와 fixture manifest를 먼저 작성한다.
2. 아직 미구현인 계약 테스트가 실패하는 것을 확인한다.
3. 이 step에서는 parser behavior를 확장하지 않는다.
4. index summary에 계약 문서와 미통과 fixture 범위를 기록한다.
## 금지사항
- Abaqus 전체 문법 지원을 약속하지 마라. 이유: 명시된 subset만 대상이다.
- unknown keyword ignore 규칙을 만들지 마라. 이유: 조용한 모델 손실을 유발한다.
- reference 원본을 고치지 마라. 이유: golden provenance를 보존해야 한다.
+55
View File
@@ -0,0 +1,55 @@
# Step 1: Part and Assembly Set Resolution
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/ABAQUS_INPUT_SUBSET.md`
- `/include/fesa/io/abaqus/`
- `/include/fesa/model/entity_set.hpp`
- `/tests/fixtures/abaqus/`
## 작업
Part 및 Assembly scope의 `NSET/ELSET`을 결정적으로 해석한다.
```cpp
struct ResolvedSet final {
std::string scope_name;
std::string set_name;
std::vector<std::int64_t> sorted_unique_labels;
};
struct SetResolutionResult final {
std::vector<ResolvedSet> sets;
std::vector<Diagnostic> diagnostics;
};
[[nodiscard]] SetResolutionResult resolve_sets(const ParsedDeck&);
```
- explicit member, `GENERATE`, nested reference, forward reference, duplicate member,
empty set, cycle, unknown set/entity, invalid range를 실패 테스트로 먼저 작성한다.
- Part와 Assembly의 같은 set 이름을 별도 scope로 허용한다.
- Assembly `INSTANCE=`는 활성 단일 Instance의 Part-local label만 lift한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "SetResolution|PartSet|AssemblySet" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. scope collision과 cycle 테스트의 실패를 확인한다.
2. graph resolution과 canonical sorted-unique 결과를 구현한다.
3. source diagnostic과 deterministic order를 assertion한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- Part와 Assembly set namespace를 합치지 마라. 이유: 이름 충돌 의미가 달라진다.
- cycle을 recursion overflow로 발견하지 마라. 이유: 명시적 cycle diagnostic이 필요하다.
- 여러 Instance key를 지원하지 마라. 이유: Phase 1 단일 Instance 범위다.
+56
View File
@@ -0,0 +1,56 @@
# Step 2: Single Instance Semantic Validation
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/ABAQUS_INPUT_SUBSET.md`
- `/include/fesa/io/abaqus/parser.hpp`
- `/include/fesa/io/abaqus/semantic_mapper.hpp`
- `/include/fesa/io/abaqus/set_resolver.hpp`
## 작업
flat/hierarchical organization과 Phase 1 단일 Instance 제한을 semantic validation으로
완성한다.
```cpp
struct ActiveInputView final {
bool flat;
std::string part_name;
std::string instance_name;
std::span<const DeckRecord> part_records;
std::span<const DeckRecord> assembly_records;
};
[[nodiscard]] ActiveInputResult select_active_input(const ParsedDeck&);
```
- flat input은 Part/Assembly가 없어야 한다.
- hierarchical input은 여러 Part를 허용하지만 Assembly와 Instance는 각각 정확히
하나여야 한다.
- missing Part, transform data, instance-local node/element, mixed organization,
wrong `INSTANCE=`를 먼저 실패 테스트로 작성한다.
- unreferenced Part가 Domain entity를 만들지 않는지 검증한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "SingleInstance|ActiveInput|SemanticScope" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 각 미지원 organization의 실패 테스트를 확인한다.
2. selection/validation만 구현하고 좌표 변환 코드는 만들지 않는다.
3. source location과 활성 Part 결과를 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- transformation data를 0으로 간주해 무시하지 마라. 이유: 계약상 명시적 오류다.
- unreferenced Part를 Domain에 병합하지 마라. 이유: 사용자 요구와 다르다.
- future multi-instance abstraction을 만들지 마라. 이유: 실제 두 번째 구현이 없다.
+48
View File
@@ -0,0 +1,48 @@
# Step 3: Material, Section, and Shear Defaults
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/ABAQUS_INPUT_SUBSET.md`
- `/include/fesa/io/abaqus/semantic_mapper.hpp`
- `/include/fesa/model/material.hpp`
- `/include/fesa/model/beam_section.hpp`
- `/include/fesa/model/domain_builder.hpp`
## 작업
전역 material과 Part-local Beam section/ELSET assignment를 complete-deck resolution으로
semantic model에 연결한다.
- `*ELASTIC`의 \(E,\nu\)에서 \(G=E/[2(1+\nu)]\)를 계산한다.
- general section의 \(A,I_y,I_{yz},I_z,J\), orientation을 읽되 \(I_{yz}=0\)만
허용한다.
- explicit transverse stiffness를 \(A_{sy},A_{sz}\)로 변환한다.
- 생략 시 \(A_{sy}=A_{sz}=5A/6\), `SCF=0`,
`ShearPropertySource::phase1_default`를 적용한다.
- nonzero `SCF`, missing/duplicate material/section assignment, invalid properties를
실패 테스트로 먼저 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "MaterialMapping|BeamSection|ShearDefault" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. material이 Part 뒤에 정의된 forward-resolution 테스트를 먼저 실패시킨다.
2. explicit/default shear 두 경로를 최소 구현한다.
3. HDF5에 저장할 source enum까지 Domain에 보존되는지 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- 임의 단면에 대한 Abaqus 보편 기본값이라고 문서화하지 마라. 이유: FESA Phase 1 가정이다.
- nonzero \(I_{yz}\)를 버리지 마라. 이유: 조용한 정식화 변경이다.
- orientation을 자동 생성하지 마라. 이유: mandatory 입력 계약이다.
+47
View File
@@ -0,0 +1,47 @@
# Step 4: Step, BC, Load, and No-op Directives
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/ABAQUS_INPUT_SUBSET.md`
- `/include/fesa/io/abaqus/`
- `/reference/cantilever beam/cantilever beam.inp`
- `/include/fesa/model/step_definition.hpp`
## 작업
단일 `*STEP/*STATIC`, Assembly set 기반 `*BOUNDARY/*CLOAD`와 명시적 no-op directive
처리를 완성한다.
- `*BOUNDARY`는 DOF 1~6의 0/비영 값을 지원한다.
- `*CLOAD`는 force/moment component 1~6을 누적한다.
- 중복 동일 BC는 canonicalize하고 충돌 prescribed value는 거부한다.
- `*HEADING`, `*PREPRINT`, `*RESTART`, `*OUTPUT`과 소유 data는 명시적으로
consume하되 Domain behavior를 만들지 않는다.
- 여러 step, `nlgeom` nonzero, unsupported output/keyword option을 실패시킨다.
- 제공된 cantilever가 11 nodes, 10 elements, 6 fixed DOFs, node 11의
\(F_z=-10000\)으로 정규화되는 통합 테스트를 먼저 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "StepMapping|Boundary|Cload|SuppliedCantilever" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 제공 샘플을 복사/변경하지 않고 public parser로 실패를 확인한다.
2. 문서화된 keyword만 구현한다.
3. expected Domain과 source diagnostic을 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- no-op 목록을 general ignore로 확장하지 마라. 이유: 모델 정의 손실을 숨긴다.
- 여러 step/load history를 추가하지 마라. 이유: Phase 1 범위 밖이다.
- reference input을 rewrite하지 마라. 이유: 검증 원본을 보존해야 한다.
@@ -0,0 +1,26 @@
{
"project": "FESA",
"phase": "beam-reference-qualification",
"steps": [
{
"step": 0,
"name": "comparison-metric-and-entity-matching",
"status": "pending"
},
{
"step": 1,
"name": "reference-csv-adapters",
"status": "pending"
},
{
"step": 2,
"name": "cantilever-reference-comparison",
"status": "pending"
},
{
"step": 3,
"name": "qualification-report",
"status": "pending"
}
]
}
@@ -0,0 +1,69 @@
# Step 0: Comparison Metric and Entity Matching
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md`
- `/include/fesa/results/result_database.hpp`
- `/include/fesa/model/entity_origin.hpp`
## 작업
CSV와 독립적인 reference comparison metric, entity position과 report를 구현한다.
```cpp
enum class ReferenceQuantity {
displacement,
reaction,
internal_force,
centroid_stress
};
struct Tolerance final { double relative; double absolute_scale; };
struct ResultPosition final {
std::string instance_name;
std::int64_t entity_label;
std::optional<std::int64_t> end_node_label;
};
struct ComparisonSample final {
ReferenceQuantity quantity;
ResultPosition position;
std::vector<double> reference;
std::vector<double> actual;
Tolerance tolerance;
};
struct ComparisonReport final {
bool passed;
double maximum_normalized_error;
std::vector<Diagnostic> failures;
};
[[nodiscard]] ComparisonReport compare_samples(
std::span<const ComparisonSample>);
```
각 scalar는 \(e_n=|a-r|/(a_{scale}+r_{tol}|r|)\)이며 \(e_n\le1\)만 통과한다.
nonfinite, duplicate position, component mismatch, unknown origin, invalid element-node
pair를 실패 테스트로 먼저 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "ComparisonMetric|EntityMatching" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. near-zero와 큰 값의 실패 테스트를 먼저 실행한다.
2. report에 quantity/entity/component/reference/actual/error를 기록한다.
3. unit-free metric과 명시 tolerance만 사용한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- NaN 비교를 통과시키지 마라. 이유: 수치 실패를 숨긴다.
- 모든 물리량에 하나의 absolute scale을 강제하지 마라. 이유: 규모가 다르다.
- CSV parsing을 이 파일에 넣지 마라. 이유: 다음 adapter 경계다.
@@ -0,0 +1,63 @@
# Step 1: Reference CSV Adapters
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md`
- `/include/fesa/validation/comparison.hpp`
- `/reference/cantilever beam/cantilever beam displacements.csv`
- `/reference/cantilever beam/cantilever beam reactions.csv`
## 작업
네 물리량의 명시적 CSV schema를 읽어 canonical reference row로 변환한다.
```cpp
struct ReferenceRow final {
ReferenceQuantity quantity;
ResultPosition position;
std::vector<double> values;
};
struct ReferenceCsvReadResult final {
std::vector<ReferenceRow> rows;
std::vector<Diagnostic> diagnostics;
};
[[nodiscard]] ReferenceCsvReadResult read_reference_csv(
ReferenceQuantity,
const std::filesystem::path&,
std::string_view single_instance_name);
```
- displacement/reaction은 제공된 whitespace 포함 Abaqus header를 읽는다.
- internal force schema:
`Part Instance Name, Element Label, Node Label, SF-SF1..SF-SF3,
SM-SM1..SM-SM3`
- stress schema:
`Part Instance Name, Element Label, Node Label, Sxx`
- 단일 Instance에서는 Instance 열 생략을 허용하고 request 이름으로 보완한다.
- `tests/fixtures/reference`에 synthetic internalforce/stress CSV를 먼저 만들고,
6개 내력 component와 centroid stress row를 실패 테스트로 고정한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "ReferenceCsv|InternalForceCsv|StressCsv" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. four-schema parser test 실패를 확인한다.
2. header/value trim과 선택적 UTF-8 BOM만 허용한다.
3. missing column, duplicate row, invalid number를 진단한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- metadata.json을 요구하지 마라. 이유: 승인된 reference 계약과 다르다.
- unknown column으로 필수 column 누락을 숨기지 마라. 이유: 잘못된 비교를 만든다.
- 내력/응력 파일이 없다는 이유로 adapter 구현을 생략하지 마라. 이유: 필수 루틴이다.
@@ -0,0 +1,50 @@
# Step 2: Cantilever Reference Comparison
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/reference/cantilever beam/cantilever beam.inp`
- `/reference/cantilever beam/cantilever beam displacements.csv`
- `/reference/cantilever beam/cantilever beam reactions.csv`
- `/include/fesa/analysis/run_solver.hpp`
- `/include/fesa/io/hdf5/reader.hpp`
- `/include/fesa/validation/reference_csv.hpp`
## 작업
제공된 계층형 캔틸레버를 production pipeline으로 해석하고 현재 존재하는 변위와
반력만 Abaqus 2024 결과와 비교한다.
- `tests/reference/cantilever_reference_test.cpp`와 reference compare CLI를 먼저
작성한다.
- comparison request는 Instance `Part-1-1`, relative tolerance `1e-5`,
displacement absolute scale `1e-10`, reaction absolute scale `1e-8`을 명시한다.
- HDF5 결과와 CSV를 public adapter로 읽어 `(Instance,Node Label)`로 join한다.
- 요청하지 않은 internal force/stress 파일을 검색하거나 pass로 보고하지 않는다.
- equilibrium과 finite result도 함께 assertion한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R CantileverReference --output-on-failure
.\out\build\windows-debug\Debug\fesa.exe solve "reference\cantilever beam\cantilever beam.inp" --output out\cantilever-beam.h5
.\out\build\windows-debug\Debug\fesa-reference-compare.exe --results out\cantilever-beam.h5 --instance Part-1-1 --displacements "reference\cantilever beam\cantilever beam displacements.csv" --reactions "reference\cantilever beam\cantilever beam reactions.csv" --relative-tolerance 1e-5 --displacement-absolute-scale 1e-10 --reaction-absolute-scale 1e-8
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. reference test가 실제 오차를 보고하며 실패하는 것을 확인한다.
2. discrepancy마다 가장 작은 analytical test를 추가한 뒤 근거 있는 kernel만 수정한다.
3. tolerance를 넓혀 결함을 숨기지 않는다.
4. 전체 테스트와 최대 정규화 오차를 index summary에 기록한다.
## 금지사항
- reference `.inp` 또는 CSV를 수정하지 마라. 이유: 원본 golden을 보존해야 한다.
- 미제공 내력/응력 Abaqus 검증을 통과했다고 주장하지 마라. 이유: 증거가 없다.
- test-only parser/solver 경로를 만들지 마라. 이유: production pipeline 검증이다.
@@ -0,0 +1,51 @@
# Step 3: Qualification Report
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/formulation/timoshenko-beam-3d.md`
- `/docs/HDF5_SCHEMA.md`
- `/tests/unit/elements/`
- `/tests/reference/`
- `/tests/fixtures/reference/`
## 작업
`docs/VALIDATION.md`에 Phase 1의 실제 검증 증거와 제한을 기록한다.
- analytical: axial, torsion, bending y/z, biaxial, shear-dominant, rigid body,
rotated frame, slenderness sweep
- physics: equilibrium, symmetry, reaction, nonzero prescribed DOF
- determinism: tested thread counts와 repeated runs
- Abaqus: 현재 cantilever displacement/reaction의 tolerance와 maximum error
- contract-only: synthetic internal-force/stress CSV schema와 component mapping
- 미제공 Abaqus internal-force/stress는 `not yet Abaqus-qualified`라고 명시한다.
보고서 수치를 새 test output에서 수집하며 수동 추정값을 쓰지 않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug --output-on-failure
ctest --preset windows-debug -R "Reference|Beam3D2|Determinism" --output-on-failure
```
모든 실행이 통과하고 보고서의 test 이름, tolerance, 최대오차와 disposition이 실제
출력과 일치해야 한다.
## 검증 절차
1. 전체 suite를 새로 실행한다.
2. 결과를 benchmark/quantity별 표에 기록한다.
3. synthetic coverage와 Abaqus-backed qualification을 명확히 분리한다.
4. index summary에 보고서 경로와 test counts를 기록한다.
## 금지사항
- 실행하지 않은 결과를 보고서에 쓰지 마라. 이유: 검증 증거가 아니다.
- Abaqus 내력/응력 qualification을 추론하지 마라. 이유: CSV가 아직 없다.
- 실패 테스트를 제외하거나 disable하지 마라. 이유: release gate를 약화한다.
@@ -0,0 +1,21 @@
{
"project": "FESA",
"phase": "deterministic-parallel-assembly",
"steps": [
{
"step": 0,
"name": "canonical-contribution-order",
"status": "pending"
},
{
"step": 1,
"name": "tbb-element-evaluation",
"status": "pending"
},
{
"step": 2,
"name": "thread-count-determinism",
"status": "pending"
}
]
}
@@ -0,0 +1,56 @@
# Step 0: Canonical Contribution Order
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/assembly/`
- `/src/fesa/assembly/`
- `/include/fesa/model/entity_origin.hpp`
## 작업
serial assembly를 변경하지 않고 병렬 계산이 사용할 canonical contribution record와
merge contract를 분리한다.
```cpp
struct MatrixContribution final {
std::size_t row;
std::size_t column;
ElementId element;
std::uint16_t local_order;
double value;
};
[[nodiscard]] std::vector<MatrixContribution> canonicalize_contributions(
std::span<const MatrixContribution>);
[[nodiscard]] SymmetricCsr merge_contributions(
std::size_t order,
std::span<const MatrixContribution> canonical);
```
- 입력 순열, 같은 row/column의 여러 element, cancellation, signed zero를 포함해
결과 CSR이 bit-for-bit 같은 테스트를 먼저 작성한다.
- 정렬 key는 row, column, stable element identity, local order다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "CanonicalContribution|DeterministicMerge" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. shuffled contribution 테스트의 실패를 확인한다.
2. stable total order와 단일 merge 구현만 추가한다.
3. serial oracle의 CSR과 bitwise 비교한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- tolerance 기반으로 assembly 값을 같다고 처리하지 마라. 이유: bitwise 재현성 계약이다.
- parallel code를 추가하지 마라. 이유: 다음 step의 책임이다.
- unordered concurrent accumulation을 준비하지 마라. 이유: 결정성을 깨뜨린다.
@@ -0,0 +1,54 @@
# Step 1: TBB Element Evaluation
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/cmake/FesaDependencies.cmake`
- `/include/fesa/assembly/assembler.hpp`
- `/include/fesa/assembly/contribution.hpp`
- `/include/fesa/elements/beam/beam3d2.hpp`
## 작업
oneTBB로 독립적인 요소 계산만 병렬화하고 contribution merge는 canonical serial
순서를 사용한다.
```cpp
struct AssemblyOptions final {
std::size_t max_threads;
std::size_t grain_size;
};
[[nodiscard]] EquationSystem assemble_parallel(
const Domain&,
const DofManager&,
AssemblyOptions);
```
- fixed Beam chain/branched Domain에서 serial과 parallel의 row offsets, column indices,
values, force vector를 bit-for-bit 비교하는 실패 테스트를 먼저 작성한다.
- worker는 thread-local contribution을 생성하고 공유 CSR values에 쓰지 않는다.
- `max_threads=1`과 2 이상을 명시적으로 제한할 수 있어야 한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "ParallelAssembly|TbbElementEvaluation" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. parallel API 부재로 실패하는 테스트를 확인한다.
2. element evaluation 범위에만 TBB를 적용한다.
3. serial/parallel bitwise 결과와 TSAN 대신 구조적 race 회피 설계를 검토한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- 공유 CSR value에 atomic add하지 마라. 이유: 합산 순서가 비결정적이다.
- PARDISO 호출을 TBB task 안에 넣지 마라. 이유: oversubscription 위험이 있다.
- 성능을 위해 tolerance를 완화하지 마라. 이유: 검증 결정성이 우선이다.
@@ -0,0 +1,46 @@
# Step 2: Thread Count Determinism
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/assembly/assembler.hpp`
- `/tests/unit/assembly/`
- `/tests/integration/assembly/`
## 작업
thread count와 반복 실행이 assembly 및 최종 선형 정적 결과를 바꾸지 않는 통합
검증과 측정용 benchmark를 추가한다.
- `tests/integration/assembly/thread_count_determinism_test.cpp`
- `tests/performance/assembly_benchmark.cpp`
- thread counts 1, 2, available concurrency에서 CSR, RHS, displacement, reaction을
bit-for-bit 비교한다.
- 최소 10회 반복으로 scheduling 변화 회귀를 확인한다.
- benchmark는 serial/parallel 시간과 element count를 출력하되 speedup을 assertion하지
않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R ThreadCountDeterminism --output-on-failure
.\out\build\windows-debug\Debug\fesa_assembly_benchmark.exe
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 다양한 thread count 통합 테스트를 먼저 실행한다.
2. 차이가 있으면 canonical key/merge 원인을 고치고 tolerance 비교로 대체하지 않는다.
3. benchmark output을 기록하고 전체 테스트를 실행한다.
4. index summary에 tested thread counts를 기록한다.
## 금지사항
- benchmark에서 속도 향상을 pass 조건으로 만들지 마라. 이유: 환경 의존적이다.
- MKL thread 수를 assembly test와 중첩해 키우지 마라. 이유: 측정이 오염된다.
- release 성능 목표를 임의로 만들지 마라. 이유: 문서화된 측정만 요구된다.
@@ -0,0 +1,26 @@
{
"project": "FESA",
"phase": "domain-and-input-skeleton",
"steps": [
{
"step": 0,
"name": "semantic-domain-and-origin-types",
"status": "pending"
},
{
"step": 1,
"name": "domain-validation",
"status": "pending"
},
{
"step": 2,
"name": "abaqus-scoped-syntax-parser",
"status": "pending"
},
{
"step": 3,
"name": "active-instance-domain-normalization",
"status": "pending"
}
]
}
+65
View File
@@ -0,0 +1,65 @@
# Step 0: Semantic Domain and Origin Types
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/include/fesa/core/entity_id.hpp`
- `/include/fesa/core/vec3.hpp`
- `/include/fesa/core/diagnostic.hpp`
## 작업
정규화된 solver semantic model의 값 타입과 불변 조회 계약을 만든다.
- `include/fesa/model/` 아래 `ids.hpp`, `entity_origin.hpp`, `node.hpp`,
`material.hpp`, `beam_section.hpp`, `beam_element.hpp`, `entity_set.hpp`,
`step_definition.hpp`, `domain.hpp`를 만든다.
- 핵심 타입:
```cpp
struct EntityOrigin final {
std::string part_name;
std::string instance_name;
std::int64_t local_label;
};
struct Node final { NodeId id; EntityOrigin origin; Vec3 position; };
struct BeamElement final {
ElementId id;
EntityOrigin origin;
std::array<NodeId, 2> nodes;
MaterialId material;
SectionId section;
};
```
- `BeamSection`은 \(A,I_y,I_z,J,A_{sy},A_{sz}\), orientation과 recovery point를
가진다. 전단값 출처는 `enum class ShearPropertySource { input, phase1_default };`
로 표현한다.
- flat mesh는 빈 `part_name/instance_name`, 계층형 mesh는 실제 이름을 사용한다.
- 먼저 값 보존과 origin 조회가 실패하는 테스트를 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "ModelTypes|EntityOrigin" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 테스트를 먼저 작성하고 compile failure를 확인한다.
2. 저장과 읽기에 필요한 최소 API만 구현한다.
3. `model` public header에 Abaqus/MKL/TBB/HDF5 타입이 없는지 확인한다.
4. focused 및 전체 테스트를 실행하고 index를 갱신한다.
## 금지사항
- parser record를 `Domain`에 저장하지 마라. 이유: syntax/semantic 경계를 깨뜨린다.
- 여러 Instance container를 만들지 마라. 이유: Phase 1은 단일 Instance다.
- equation 번호를 Node/Element에 저장하지 마라. 이유: DofManager 책임이다.
+63
View File
@@ -0,0 +1,63 @@
# Step 1: Domain Validation
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/include/fesa/model/`
- `/tests/unit/model/`
이전 step의 타입 이름과 필드를 변경하지 말고 builder validation을 추가하라.
## 작업
`DomainBuilder`가 모든 semantic entity를 검증한 뒤 불변 `Domain` 하나를 생성하게
한다.
```cpp
struct DomainBuildResult final {
std::optional<Domain> domain;
std::vector<Diagnostic> diagnostics;
};
class DomainBuilder final {
public:
void add_node(Node);
void add_material(IsotropicElastic);
void add_section(BeamSection);
void add_beam_element(BeamElement);
void add_node_set(NodeSet);
void add_element_set(ElementSet);
void set_step(StepDefinition);
[[nodiscard]] DomainBuildResult build() &&;
};
```
- 중복 internal ID, 중복 origin, missing reference, invalid \(E,\nu,A,I,J,A_s\),
nonfinite 값, zero-length element, invalid orientation, section/material 누락,
충돌 BC를 각각 실패 테스트로 먼저 작성한다.
- 내부 dense lookup과 origin lookup을 만들되 public mutable access는 제공하지 않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "DomainBuilder|DomainValidation" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 각 거부 조건의 실패 테스트를 먼저 확인한다.
2. 하나의 오류가 다른 오류를 숨기지 않도록 가능한 진단을 수집한다.
3. valid Domain test와 전체 CTest를 실행한다.
4. index summary에 검증 규칙과 생성 파일을 기록한다.
## 금지사항
- 잘못된 입력값을 자동 보정하지 마라. 이유: explicit diagnostic 계약을 위반한다.
- solver 또는 parser validation을 이 builder에 넣지 마라. 이유: 모듈 책임이 다르다.
- mutable Domain accessor를 추가하지 마라. 이유: 해석 중 모델 불변성을 깨뜨린다.
+74
View File
@@ -0,0 +1,74 @@
# Step 2: Abaqus Scoped Syntax Parser
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/include/fesa/core/source_location.hpp`
- `/include/fesa/core/diagnostic.hpp`
## 작업
Abaqus keyword를 해석하지 않고 scope가 보존된 syntax tree로 읽는 parser를 만든다.
```cpp
struct DeckRecord final {
std::string keyword;
std::map<std::string, std::string, std::less<>> parameters;
std::vector<std::vector<std::string>> data;
SourceLocation source;
};
struct ParsedPart final {
std::string name;
std::vector<DeckRecord> records;
SourceLocation source;
};
struct ParsedInstance final {
std::string name;
std::string part_name;
std::vector<std::vector<std::string>> transform_data;
SourceLocation source;
};
struct ParsedAssembly final {
std::string name;
std::vector<ParsedInstance> instances;
std::vector<DeckRecord> records;
SourceLocation source;
};
struct ParsedDeck final {
std::vector<DeckRecord> global_records;
std::vector<ParsedPart> parts;
std::optional<ParsedAssembly> assembly;
};
[[nodiscard]] ParseDeckResult parse_deck(const std::filesystem::path&);
```
- flat fixture와 단일 Part/Assembly/Instance fixture를 먼저 만든다.
- case-insensitive keyword, comment, blank line, comma field, UTF-8, source line,
scope 종료 오류를 테스트한다.
- 이 step은 syntax만 파싱하며 active Part를 선택하지 않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "AbaqusParser|ScopedDeck" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. parser test를 작성하고 실패를 확인한다.
2. `io/abaqus`에 lexer/parser만 최소 구현한다.
3. source location과 scope tree를 assertion한다.
4. 전체 테스트 후 index를 갱신한다.
## 금지사항
- Domain을 생성하지 마라. 이유: 다음 semantic normalization step의 책임이다.
- unknown keyword를 일반적으로 무시하지 마라. 이유: 명시적 입력 계약을 훼손한다.
- Instance 변환을 적용하지 마라. 이유: Phase 1에서 거부할 syntax 정보로 보존한다.
+52
View File
@@ -0,0 +1,52 @@
# Step 3: Active Instance Domain Normalization
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md`
- `/include/fesa/model/domain_builder.hpp`
- `/include/fesa/io/abaqus/deck_record.hpp`
- `/include/fesa/io/abaqus/parser.hpp`
- `/tests/fixtures/abaqus/minimal_cantilever.inp`
- `/tests/fixtures/abaqus/minimal_part_instance_cantilever.inp`
## 작업
flat 또는 단일 무변환 Instance 입력을 동일한 `Domain`으로 변환한다.
```cpp
[[nodiscard]] DomainBuildResult map_deck_to_domain(const ParsedDeck& deck);
```
- 먼저 두 fixture가 동등한 활성 절점·요소·재료·단면·하중·BC를 만드는 통합
테스트를 작성한다.
- 계층형 deck은 정확히 하나의 Assembly와 Instance를 요구한다.
- transform data가 있거나 Part reference가 없으면 source diagnostic을 반환한다.
- Instance가 참조하지 않는 Part는 Domain에 포함하지 않는다.
- origin에는 실제 Part/Instance/local label을 보존한다.
- 전단강성이 없으면 \(A_{sy}=A_{sz}=5A/6\), `SCF=0`,
`ShearPropertySource::phase1_default`를 사용한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "DeckToDomain|ActiveInstance" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. public parser와 mapper를 사용하는 실패 테스트를 확인한다.
2. syntax record를 복제해 Domain에 저장하지 않고 semantic 값으로 변환한다.
3. unreferenced Part exclusion과 provenance를 assertion한다.
4. focused/full test 후 index summary를 갱신한다.
## 금지사항
- 여러 Instance를 평탄화하지 마라. 이유: Phase 1 범위 밖이다.
- 변환 좌표를 조용히 무시하지 마라. 이유: 잘못된 해석 모델을 만든다.
- 테스트에서 Domain을 직접 구성하지 마라. 이유: production 경로를 우회한다.
@@ -0,0 +1,21 @@
{
"project": "FESA",
"phase": "equation-and-linear-solve",
"steps": [
{
"step": 0,
"name": "symmetric-csr-assembly",
"status": "pending"
},
{
"step": 1,
"name": "essential-bc-elimination",
"status": "pending"
},
{
"step": 2,
"name": "pardiso-linear-solver",
"status": "pending"
}
]
}
+58
View File
@@ -0,0 +1,58 @@
# Step 0: Symmetric CSR Assembly
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/model/domain.hpp`
- `/include/fesa/fem/dof_manager.hpp`
- `/include/fesa/elements/beam/beam3d2.hpp`
## 작업
Beam local contribution으로부터 deterministic serial symmetric CSR과 full load vector를
조립한다.
```cpp
struct SymmetricCsr final {
std::size_t order;
std::vector<std::int32_t> row_offsets;
std::vector<std::int32_t> column_indices;
std::vector<double> values;
};
struct EquationSystem final {
SymmetricCsr stiffness;
std::vector<double> force;
};
[[nodiscard]] EquationSystem assemble_serial(
const Domain&,
const DofManager&);
```
- sparsity pattern builder와 numeric contribution merge를 분리한다.
- `(row,column,element-origin,local-order)`의 안정된 순서로 합산한다.
- hand-calculated 2-element system, duplicate contribution, external ID 순서 변화,
CSR invariant를 실패 테스트로 먼저 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "SparsePattern|SerialAssembly|SymmetricCsr" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. expected CSR 구조와 값을 고정한 실패 테스트를 실행한다.
2. pattern과 numeric assembly를 최소 구현한다.
3. row offset, sorted column, upper/lower storage 계약을 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- 공유 CSR에 병렬 누적하지 마라. 이유: 이 phase는 serial oracle을 만든다.
- PARDISO를 호출하지 마라. 이유: backend step의 책임이다.
- element formulation을 assembly에 복제하지 마라. 이유: 모듈 경계를 깨뜨린다.
+56
View File
@@ -0,0 +1,56 @@
# Step 1: Essential BC Elimination
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/assembly/symmetric_csr.hpp`
- `/include/fesa/assembly/equation_system.hpp`
- `/include/fesa/fem/dof_manager.hpp`
- `/include/fesa/model/step_definition.hpp`
## 작업
0과 비영 지정변위를 지원하는 essential-BC elimination과 full-vector 복원을 구현한다.
```cpp
struct ReducedSystem final {
SymmetricCsr stiffness;
std::vector<double> force;
std::vector<std::size_t> free_to_full;
std::vector<double> prescribed_full;
};
[[nodiscard]] ConstraintResult eliminate_essential_bcs(
const EquationSystem& original,
const DofManager& dofs,
std::span<const PrescribedDof> prescribed);
[[nodiscard]] std::vector<double> recover_reaction(
const EquationSystem& original,
std::span<const double> full_displacement);
```
- 작은 hand calculation으로 RHS shift, 0/비영 prescribed value, all constrained,
충돌 조건, \(r=Ku-f\) 반력 복원을 먼저 테스트한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "EssentialBc|ConstraintElimination|Reaction" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 비영 지정값 테스트의 실패를 먼저 확인한다.
2. 원래 EquationSystem을 보존한 채 reduced system을 생성한다.
3. 복원 변위와 원래 평형식 반력을 assertion한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- penalty나 큰 수를 사용하지 마라. 이유: 승인된 elimination 정책과 다르다.
- 반력을 reduced matrix에서 계산하지 마라. 이유: 원래 \(K,u,f\)가 필요하다.
- MPC/Lagrange multiplier를 추가하지 마라. 이유: 범위 밖이다.
+65
View File
@@ -0,0 +1,65 @@
# Step 2: PARDISO Linear Solver
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/cmake/FesaDependencies.cmake`
- `/include/fesa/assembly/symmetric_csr.hpp`
- `/include/fesa/core/diagnostic.hpp`
## 작업
MKL PARDISO를 RAII adapter 뒤에 격리하고 symmetric positive-definite reduced system을
푼다.
```cpp
struct LinearSolveResult final {
std::vector<double> solution;
double relative_residual;
std::vector<Diagnostic> diagnostics;
};
class LinearSolver {
public:
virtual ~LinearSolver() = default;
[[nodiscard]] virtual LinearSolveResult solve(
const SymmetricCsr&,
std::span<const double> rhs) = 0;
};
class PardisoLinearSolver final : public LinearSolver {
public:
PardisoLinearSolver();
~PardisoLinearSolver() override;
[[nodiscard]] LinearSolveResult solve(
const SymmetricCsr&,
std::span<const double>) override;
};
```
- 3x3 SPD, repeated solve, invalid CSR, dimension mismatch, singular matrix를 먼저
테스트한다.
- `mtype=2`, LP64 index, `iparm[34]=1`, matrix checker, analysis/factor/solve/release
phase를 사용한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Pardiso|LinearSolver" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. adapter test의 link/behavior 실패를 확인한다.
2. 모든 MKL handle/workspace를 RAII로 해제한다.
3. 해와 상대잔차를 독립 계산으로 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- MKL 타입을 `LinearSolver` public contract에 노출하지 마라. 이유: backend 격리다.
- singular system을 임의 regularization하지 마라. 이유: 모델 오류를 숨긴다.
- PARDISO 실행 중 TBB task를 중첩하지 마라. 이유: oversubscription 정책 위반이다.
+26
View File
@@ -0,0 +1,26 @@
{
"project": "FESA",
"phase": "fem-and-beam-kernel",
"steps": [
{
"step": 0,
"name": "quadrature-and-shape-functions",
"status": "pending"
},
{
"step": 1,
"name": "dof-manager",
"status": "pending"
},
{
"step": 2,
"name": "beam-local-frame",
"status": "pending"
},
{
"step": 3,
"name": "timoshenko-stiffness-kernel",
"status": "pending"
}
]
}
+47
View File
@@ -0,0 +1,47 @@
# Step 0: Quadrature and Shape Functions
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/include/fesa/core/vec3.hpp`
## 작업
특정 analysis에 종속되지 않는 1D Gauss quadrature와 2절점 선형 shape function을
`fem` 모듈에 구현한다.
```cpp
struct GaussPoint1D final { double xi; double weight; };
[[nodiscard]] std::span<const GaussPoint1D> gauss_rule_1d(int order);
[[nodiscard]] std::array<double, 2> line2_shape(double xi);
[[nodiscard]] std::array<double, 2> line2_shape_derivative(double xi);
[[nodiscard]] double line2_jacobian(double length);
```
- partition of unity, endpoint interpolation, derivative sum zero, 1점/2점 적분의
정확도, length/2 Jacobian, invalid order/length를 실패 테스트로 먼저 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Quadrature|ShapeFunction|Jacobian" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 수학 invariant 테스트의 실패를 확인한다.
2. 고정 크기 값 타입과 최소 함수만 구현한다.
3. tolerance 근거를 테스트 이름 또는 주석에 명시한다.
4. 전체 CTest와 index 갱신을 수행한다.
## 금지사항
- Beam stiffness를 이 step에 구현하지 마라. 이유: 수학 primitive 경계를 유지한다.
- runtime quadrature registry를 만들지 마라. 이유: 1점과 2점만 필요하다.
- 잘못된 길이에 임의 epsilon을 더하지 마라. 이유: model 오류를 숨긴다.
+57
View File
@@ -0,0 +1,57 @@
# Step 1: DOF Manager
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/model/domain.hpp`
- `/include/fesa/model/step_definition.hpp`
- `/include/fesa/fem/`
## 작업
절점당 6자유도와 constrained/free equation numbering을 전담하는 `DofManager`
구현한다.
```cpp
enum class NodeDof : std::uint8_t { ux, uy, uz, rx, ry, rz };
struct DofAddress final { NodeId node; NodeDof dof; };
class DofManager final {
public:
[[nodiscard]] static DofManager build(const Domain&);
[[nodiscard]] std::size_t full_dof_count() const noexcept;
[[nodiscard]] std::size_t free_equation_count() const noexcept;
[[nodiscard]] std::optional<std::size_t> equation(DofAddress) const;
[[nodiscard]] std::array<std::size_t, 12> element_full_dofs(
const BeamElement&) const;
[[nodiscard]] std::vector<double> reconstruct_full(
std::span<const double> reduced) const;
};
```
- external label 순서와 무관한 deterministic numbering, 비영 지정값, full/reduced
reconstruction, invalid DOF를 실패 테스트로 먼저 고정한다.
- equation ID를 Node/Element에 쓰지 않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "DofManager|EquationNumbering" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 실패 테스트를 먼저 실행한다.
2. Domain 읽기 전용 view만 사용해 numbering을 구현한다.
3. constrained/free mapping과 reconstruction을 직접 assertion한다.
4. 전체 테스트와 index 갱신을 수행한다.
## 금지사항
- sparse matrix pattern을 소유하지 마라. 이유: assembly 책임이다.
- Node/Element에 equation ID를 저장하지 마라. 이유: 아키텍처 규칙 위반이다.
- MPC나 penalty 자유도를 추가하지 마라. 이유: Phase 1 범위 밖이다.
+53
View File
@@ -0,0 +1,53 @@
# Step 2: Beam Local Frame
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/core/vec3.hpp`
- `/include/fesa/model/beam_section.hpp`
- `/include/fesa/model/beam_element.hpp`
## 작업
두 절점과 mandatory orientation vector로 오른손 직교 Beam 국부 기저와 12x12
좌표변환을 계산한다.
```cpp
struct BeamFrame final { Vec3 ex; Vec3 ey; Vec3 ez; };
struct BeamFrameResult final {
std::optional<BeamFrame> frame;
std::vector<Diagnostic> diagnostics;
};
[[nodiscard]] BeamFrameResult make_beam_frame(
const Vec3& first,
const Vec3& second,
const Vec3& orientation);
[[nodiscard]] Matrix12 beam_transformation(const BeamFrame&);
```
- 축 방향 정규화, Gram-Schmidt, 오른손성, 직교성, 회전 불변성을 테스트한다.
- zero length, zero orientation, orientation parallel to element axis를 실패시킨다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "BeamFrame|BeamTransformation" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 축 정렬 및 임의 회전 테스트의 실패를 확인한다.
2. tolerance를 scale-aware하게 적용한다.
3. \(R R^T=I\), determinant \(+1\), 변환 energy invariant를 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- orientation을 자동 추측하지 마라. 이유: 입력 계약에서 필수다.
- degenerate vector를 임의 축으로 대체하지 마라. 이유: 모델 오류를 숨긴다.
- element stiffness를 추가하지 마라. 이유: 다음 kernel step의 책임이다.
+59
View File
@@ -0,0 +1,59 @@
# Step 3: Timoshenko Stiffness Kernel
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/include/fesa/fem/`
- `/include/fesa/model/material.hpp`
- `/include/fesa/model/beam_section.hpp`
- `/docs/formulation/timoshenko-beam-3d.md`가 있으면 전체
## 작업
먼저 `docs/formulation/timoshenko-beam-3d.md`에 자유도 순서, 변형률, 부호,
constitutive matrix, Jacobian과 선택적 감차적분 식을 작성한다. 그 식으로 실제
2절점 3D isoparametric Timoshenko Beam stiffness를 구현한다.
```cpp
struct Beam3D2Input final {
std::array<Vec3, 2> coordinates;
IsotropicElastic material;
BeamSection section;
};
struct Beam3D2Contribution final {
Matrix12 local_stiffness;
Matrix12 global_stiffness;
BeamFrame frame;
};
[[nodiscard]] BeamKernelResult compute_beam3d2(const Beam3D2Input&);
```
- 축·굽힘·비틀림은 2점, 전단은 1점 Gauss 적분한다.
- \(G=E/[2(1+\nu)]\)를 사용한다.
- 먼저 대칭성, 강체운동 zero energy, 축/비틀림/굽힘 해석해, shear-dominant,
세장비 sweep과 좌표회전 invariant 테스트를 실패시킨다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Beam3D2|Timoshenko|RigidBody" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 정식화 문서를 production code보다 먼저 확정한다.
2. 실패 테스트를 실행해 가짜 matrix로 통과하지 않음을 확인한다.
3. 최소 kernel을 구현하고 focused/full test를 실행한다.
4. 식과 코드의 DOF/component 순서를 대조하고 index를 갱신한다.
## 금지사항
- 닫힌형 가짜 stiffness를 임시로 넣지 마라. 이유: 파이프라인 수치 신뢰성을 훼손한다.
- shear term을 2점 적분하지 마라. 이유: 승인된 selective integration과 다르다.
- 비선형, 워핑, offset 또는 \(I_{yz}\)를 추가하지 마라. 이유: Phase 1 밖이다.
+44
View File
@@ -0,0 +1,44 @@
{
"phases": [
{
"dir": "solver-bootstrap",
"status": "pending"
},
{
"dir": "domain-and-input-skeleton",
"status": "pending"
},
{
"dir": "fem-and-beam-kernel",
"status": "pending"
},
{
"dir": "equation-and-linear-solve",
"status": "pending"
},
{
"dir": "results-and-pipeline",
"status": "pending"
},
{
"dir": "abaqus-subset-completion",
"status": "pending"
},
{
"dir": "deterministic-parallel-assembly",
"status": "pending"
},
{
"dir": "result-contract-completion",
"status": "pending"
},
{
"dir": "beam-reference-qualification",
"status": "pending"
},
{
"dir": "internal-release",
"status": "pending"
}
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"project": "FESA",
"phase": "internal-release",
"steps": [
{
"step": 0,
"name": "release-checklist",
"status": "pending"
},
{
"step": 1,
"name": "cmake-install-package",
"status": "pending"
},
{
"step": 2,
"name": "install-tree-smoke-test",
"status": "pending"
},
{
"step": 3,
"name": "phase1-scale-benchmark",
"status": "pending"
},
{
"step": 4,
"name": "release-evidence-gate",
"status": "pending"
}
]
}
+54
View File
@@ -0,0 +1,54 @@
# Step 0: Release Checklist
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/VALIDATION.md`
- `/docs/HDF5_SCHEMA.md`
- `/docs/ABAQUS_INPUT_SUBSET.md`
- `/CMakePresets.json`
## 작업
packaging 전에 `docs/BUILDING.md`, `docs/INPUT_FORMAT.md`,
`docs/RELEASE_CHECKLIST.md`를 작성한다.
체크리스트는 다음 증거 위치와 command를 포함해야 한다.
- MSVC v143 x64, C++20 및 dependency versions
- Debug/Release configure, build, zero-warning, nonzero CTest count
- flat 및 단일 무변환 Instance example
- HDF5 schema inspection
- 현재 Abaqus displacement/reaction comparison
- synthetic internal-force/stress adapter coverage
- deterministic thread-count test
- 100k-DOF scale measurement
- runtime DLL inventory와 clean install-tree smoke test
PRD section 8 각 항목에 고유 checklist ID를 부여한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug --output-on-failure
```
문서의 모든 command가 현재 target/preset 이름과 일치하고 미제공 Abaqus
내력·응력을 qualification 완료로 표시하지 않아야 한다.
## 검증 절차
1. PRD release criterion을 하나씩 checklist에 매핑한다.
2. 현재 test/target 이름을 CMake에서 재확인한다.
3. 증거가 없는 항목은 pass로 쓰지 않고 미실행 상태로 둔다.
4. index summary에 세 문서와 checklist ID 범위를 기록한다.
## 금지사항
- installer나 외부 고객 SDK를 약속하지 마라. 이유: 내부 배포 범위다.
- evidence 없는 checkbox를 완료 표시하지 마라. 이유: release gate를 왜곡한다.
- per-model metadata 파일을 다시 요구하지 마라. 이유: 승인된 검증 계약과 다르다.
+49
View File
@@ -0,0 +1,49 @@
# Step 1: CMake Install Package
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/BUILDING.md`
- `/docs/RELEASE_CHECKLIST.md`
- `/CMakeLists.txt`
- `/CMakePresets.json`
## 작업
`cmake --install`로 내부 배포용 install tree를 만든다.
- `cmake/install.cmake`, `cmake/FESAConfig.cmake.in`을 추가한다.
- install tree에 `fesa.exe`, `fesa_core` static library, public headers,
CMake package config, required runtime DLL inventory, example input,
`HDF5_SCHEMA.md`, `INPUT_FORMAT.md`, `VALIDATION.md`를 포함한다.
- absolute build path가 install config에 남지 않는 실패 검사부터 작성한다.
- Phase 1 public ABI compatibility를 약속하지 않는다.
## Acceptance Criteria
```powershell
cmake --preset windows-release
cmake --build --preset windows-release
ctest --preset windows-release --output-on-failure
cmake --install out\build\windows-release --config Release --prefix out\package\fesa
Get-ChildItem -Recurse out\package\fesa
```
install tree가 build source tree 밖에서 사용 가능한 상대 경로와 명시적 runtime inventory를
가져야 한다.
## 검증 절차
1. install manifest 검사 실패를 먼저 확인한다.
2. 최소 install/export rule을 구현한다.
3. package tree와 CMake config의 절대 경로 누출을 확인한다.
4. index summary에 install manifest를 기록한다.
## 금지사항
- registry write나 MSI installer를 추가하지 마라. 이유: 배포 범위 밖이다.
- dependency를 package 중 다운로드하지 마라. 이유: 사전 설치 정책 위반이다.
- Debug/Release binary를 혼합하지 마라. 이유: runtime 불일치 위험이 있다.
+44
View File
@@ -0,0 +1,44 @@
# Step 2: Install-tree Smoke Test
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/BUILDING.md`
- `/docs/RELEASE_CHECKLIST.md`
- `/cmake/FESAConfig.cmake.in`
- `/cmake/install.cmake`
## 작업
source/build tree를 참조하지 않는 clean install consumer와 CLI smoke test를 만든다.
- `tests/install/consumer/CMakeLists.txt`, `tests/install/consumer/main.cpp`,
`tests/install/install_tree_smoke.cmake`를 만든다.
- installed `FESAConfig.cmake`로 작은 consumer를 configure/link한다.
- installed CLI의 `--version`, example solve, 생성 HDF5 public inspection을 실행한다.
- source include path나 build library path를 숨긴 상태에서 먼저 실패를 확인한다.
## Acceptance Criteria
```powershell
cmake --preset windows-release
cmake --build --preset windows-release
ctest --preset windows-release -R InstallTreeSmoke --output-on-failure
ctest --preset windows-release --output-on-failure
```
## 검증 절차
1. 설치 전 smoke test 실패를 확인한다.
2. install tree만 사용하도록 test working directory와 environment를 격리한다.
3. consumer link, CLI solve, HDF5 open을 모두 확인한다.
4. 전체 Release test와 index를 갱신한다.
## 금지사항
- source directory include를 fallback으로 넣지 마라. 이유: packaging 결함을 숨긴다.
- PATH의 개발용 `fesa.exe`를 실행하지 마라. 이유: installed binary 검증이 아니다.
- external dependency installer를 만들지 마라. 이유: inventory만 제공한다.
+46
View File
@@ -0,0 +1,46 @@
# Step 3: Phase 1 Scale Benchmark
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/RELEASE_CHECKLIST.md`
- `/include/fesa/analysis/run_solver.hpp`
- `/tests/performance/assembly_benchmark.cpp`
## 작업
약 100,000 DOF의 deterministic Beam chain 모델로 메모리와 시간을 측정하는
`tests/performance/phase1_scale_benchmark.cpp`를 만든다.
- model generation, parsing/Domain build, assembly, PARDISO solve, recovery, HDF5 write
시간을 구분해 출력한다.
- peak working set 또는 Windows에서 재현 가능한 memory metric을 기록한다.
- benchmark는 finite result, equilibrium, expected DOF count와 정상 종료만 assertion한다.
- 임의 성능 기준이나 speedup을 pass 조건으로 만들지 않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-release
.\out\build\windows-release\Release\fesa_phase1_scale_benchmark.exe
ctest --preset windows-release --output-on-failure
```
출력에 모델 크기, 각 stage 시간, memory metric, thread 설정, solver 설정이 포함되어야
한다.
## 검증 절차
1. 작은 model에서 benchmark correctness test를 먼저 실패시킨다.
2. 동일 generator로 목표 크기를 실행한다.
3. 측정값과 환경을 release checklist에 기록한다.
4. index summary에 결과 위치와 실제 DOF count를 기록한다.
## 금지사항
- benchmark를 unit test timeout에 묶지 마라. 이유: 머신 성능에 따라 달라진다.
- 결과 정확성 검사를 생략하지 마라. 이유: 빠른 오답은 성능 증거가 아니다.
- 실제 측정 없이 목표 시간을 만들지 마라. 이유: 요구사항에 근거가 없다.
+57
View File
@@ -0,0 +1,57 @@
# Step 4: Release Evidence Gate
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/VALIDATION.md`
- `/docs/BUILDING.md`
- `/docs/INPUT_FORMAT.md`
- `/docs/RELEASE_CHECKLIST.md`
- `/docs/HDF5_SCHEMA.md`
## 작업
새 실행 증거로 PRD section 8의 내부 배포 수용 조건을 모두 판정한다.
- Debug와 Release configure/build/test를 각각 실행한다.
- test count가 0이 아닌지 확인한다.
- Harness Python self-test를 실행한다.
- reference, determinism, install-tree, HDF5 inspection, scale benchmark 결과를
checklist ID에 연결한다.
- 현재 검증 범위가 Abaqus displacement/reaction이며 내력/응력은 synthetic adapter
coverage임을 release note에 명시한다.
- 하나라도 증거가 없거나 실패하면 release를 완료 처리하지 말고 `blocked` 또는
`error`로 기록한다.
## Acceptance Criteria
```powershell
uv run --with pytest python -m pytest -v -rs
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug --output-on-failure
cmake --preset windows-release
cmake --build --preset windows-release
ctest --preset windows-release --output-on-failure
.\out\build\windows-release\Release\fesa_phase1_scale_benchmark.exe
cmake --install out\build\windows-release --config Release --prefix out\package\fesa
```
모든 명령이 종료 코드 0이고 release checklist의 각 criterion이 해당 출력 또는
validation report 항목으로 추적되어야 한다.
## 검증 절차
1. 위 명령을 생략 없이 새로 실행한다.
2. warning, test count, reference maximum error, benchmark와 install manifest를 기록한다.
3. 성공한 경우에만 step/phase를 `completed`로 갱신한다.
4. 미제공 외부 데이터가 필요하면 범위를 확대하지 말고 정확한 blocker를 기록한다.
## 금지사항
- 실패 테스트를 disable하거나 제외하지 마라. 이유: release 증거를 조작한다.
- 구현되지 않은 Abaqus 내력/응력 qualification을 선언하지 마라. 이유: golden이 없다.
- 자동 push하지 마라. 이유: 사용자가 `--push`를 명시한 경우에만 허용된다.
@@ -0,0 +1,21 @@
{
"project": "FESA",
"phase": "result-contract-completion",
"steps": [
{
"step": 0,
"name": "beam-element-end-recovery",
"status": "pending"
},
{
"step": 1,
"name": "complete-result-contract",
"status": "pending"
},
{
"step": 2,
"name": "self-contained-hdf5",
"status": "pending"
}
]
}
@@ -0,0 +1,56 @@
# Step 0: Beam Element-end Recovery
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/formulation/timoshenko-beam-3d.md`
- `/include/fesa/elements/beam/beam3d2.hpp`
- `/include/fesa/model/beam_section.hpp`
## 작업
요소 양 끝 절점에서 section strain/resultant, 도심 응력과 선택 회복점 축응력을
계산한다.
```cpp
struct BeamSectionResult final {
double xi;
NodeId end_node;
std::array<double, 6> section_strain;
std::array<double, 6> section_force;
double centroid_sigma_xx;
std::vector<double> sigma_xx;
};
[[nodiscard]] std::vector<BeamSectionResult> recover_beam3d2(
const Beam3D2Input&,
std::span<const double, 12> element_displacement,
std::span<const std::array<double, 2>> recovery_points);
```
- component 순서는 \(N,V_y,V_z,T,M_y,M_z\)다.
- `centroid_sigma_xx=N/A`; 회복점은 axial+bending \(\sigma_{xx}\)만 계산한다.
- 순수 축/비틀림/각 축 굽힘/이축 굽힘, 양 끝 부호, 회복점 순서를 먼저 테스트한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "BeamRecovery|CentroidStress|SectionForce" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. hand-calculated recovery test 실패를 확인한다.
2. stiffness와 같은 frame/부호 convention을 재사용한다.
3. 도심에서 bending contribution이 0인지 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- point shear/torsional stress를 출력하지 마라. 이유: 단면 형상 정보가 부족하다.
- Abaqus column 이름을 element kernel에 넣지 마라. 이유: validation adapter 책임이다.
- 절점별 내력 부호를 임의 절댓값으로 바꾸지 마라. 이유: 평형 검증을 깨뜨린다.
@@ -0,0 +1,55 @@
# Step 1: Complete Result Contract
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/results/result_database.hpp`
- `/include/fesa/elements/beam/beam3d2.hpp`
- `/include/fesa/model/entity_origin.hpp`
## 작업
ResultDatabase에 element-end Beam 결과, local frame, model provenance와 diagnostics를
추가한다.
```cpp
struct BeamElementFrame final {
ElementId element;
EntityOrigin origin;
BeamFrame local_frame;
std::array<BeamSectionResult, 2> end_results;
};
struct ElementFrame final {
std::vector<BeamElementFrame> beams;
};
```
- `ResultFrame``ElementFrame element`를 추가한다.
- node/element origin, field coordinate system, component labels와 ordering을 명시한다.
- duplicate element/end node, wrong connectivity, nonfinite result, mismatched recovery
point count를 실패 테스트로 먼저 작성한다.
- LinearStaticAnalysis가 production recovery를 호출해 ResultDatabase를 채우게 한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "CompleteResultContract|ElementFrame" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. result validation과 analysis integration test 실패를 확인한다.
2. 의미 타입과 orchestration만 수정한다.
3. element-end/node connectivity와 origin 보존을 assertion한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- CSV-specific field를 ResultDatabase에 넣지 마라. 이유: 결과 semantic 모델을 오염시킨다.
- history/velocity를 빈 구조로 추가하지 마라. 이유: Phase 1에서 사용하지 않는다.
- recovery를 analysis 코드에 복제하지 마라. 이유: element kernel 계약을 재사용해야 한다.
@@ -0,0 +1,47 @@
# Step 2: Self-contained HDF5
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/HDF5_SCHEMA.md`
- `/include/fesa/results/result_database.hpp`
- `/include/fesa/io/hdf5/`
## 작업
HDF5 schema와 reader/writer를 완성해 파일 하나로 모델·설정·전체 Phase 1 결과를
재구성할 수 있게 한다.
- model: coordinates, connectivity, Part/Instance/local labels, sets, materials,
sections, orientation, applied shear values와 input/default source
- analysis: single step, BC, load, solver settings
- results: nodal displacement/reaction, Beam local frame, 양 끝 section
strain/force, centroid `Sxx`, recovery-point `Sxx`, diagnostics
- `docs/HDF5_SCHEMA.md`의 dataset rank/type/component/coordinate attribute를 먼저
갱신한다.
- public reader만으로 모든 항목을 재구성하는 실패 통합 테스트를 작성한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R SelfContainedHdf5 --output-on-failure
h5ls -r .\out\build\windows-debug\Testing\Temporary\fesa-self-contained.h5
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. complete schema round-trip test 실패를 확인한다.
2. schema version 호환성 규칙을 문서와 코드에 함께 반영한다.
3. public reader와 h5ls로 구조를 독립 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- 원본 `.inp` 경로만 저장하고 모델 데이터를 생략하지 마라. 이유: 자기완결 계약이다.
- HDF5 object를 result model에 노출하지 마라. 이유: adapter 경계를 깨뜨린다.
- schema를 무버전 변경하지 마라. 이유: reader 호환성을 잃는다.
+26
View File
@@ -0,0 +1,26 @@
{
"project": "FESA",
"phase": "results-and-pipeline",
"steps": [
{
"step": 0,
"name": "result-database",
"status": "pending"
},
{
"step": 1,
"name": "minimal-hdf5-schema",
"status": "pending"
},
{
"step": 2,
"name": "linear-static-analysis",
"status": "pending"
},
{
"step": 3,
"name": "cli-pipeline-integration",
"status": "pending"
}
]
}
+60
View File
@@ -0,0 +1,60 @@
# Step 0: Result Database
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/model/domain.hpp`
- `/include/fesa/core/diagnostic.hpp`
## 작업
HDF5와 독립적인 최소 Phase 1 result semantic model을 만든다.
```cpp
struct NodalFrame final {
std::vector<NodeId> node_ids;
std::vector<std::array<double, 6>> displacement;
std::vector<std::array<double, 6>> reaction;
};
struct ResultFrame final {
double step_time;
NodalFrame nodal;
std::vector<Diagnostic> diagnostics;
};
struct ResultStep final {
std::string name;
std::vector<ResultFrame> frames;
};
struct ResultDatabase final {
std::string schema_version;
std::vector<ResultStep> steps;
};
```
- size mismatch, duplicate node, nonfinite field, duplicate step/frame을 실패 테스트로
먼저 작성한다.
- 이 step에는 element result를 미리 만들지 않는다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "ResultDatabase|NodalFrame" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. 유효/무효 result model 테스트를 먼저 실행한다.
2. 불변 읽기 계약에 필요한 최소 저장만 구현한다.
3. HDF5 include가 없는지 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- HDF5 handle을 result model에 넣지 마라. 이유: semantic/adaptor 경계를 깨뜨린다.
- velocity, acceleration, temperature를 추가하지 마라. 이유: Phase 1에서 사용하지 않는다.
- 빈 element output hierarchy를 만들지 마라. 이유: 필요한 phase에서만 실체화한다.
+56
View File
@@ -0,0 +1,56 @@
# Step 1: Minimal HDF5 Schema
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/results/result_database.hpp`
- `/include/fesa/model/domain.hpp`
- `/cmake/FesaDependencies.cmake`
## 작업
먼저 `docs/HDF5_SCHEMA.md`에 schema `1.0.0`의 최소 group/dataset/attribute 계약을
작성하고 HDF5 writer/reader round trip을 구현한다.
```cpp
struct Hdf5ReadResult final {
std::optional<ResultDatabase> database;
std::vector<Diagnostic> diagnostics;
};
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
const std::filesystem::path&,
const Domain&,
const ResultDatabase&);
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
const std::filesystem::path&);
```
- schema/version, node origin `(part,instance,local label)`, dense ID map, 좌표,
connectivity, shear property source, nodal displacement/reaction을 round trip한다.
- 먼저 public reader로 모든 값을 재확인하는 실패 테스트를 작성한다.
- 모든 `hid_t`는 move-only RAII wrapper로 관리한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Hdf5|ResultRoundTrip" --output-on-failure
h5ls -r .\out\build\windows-debug\Testing\Temporary\fesa-round-trip.h5
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. schema 문서를 writer보다 먼저 작성한다.
2. round-trip test 실패를 확인한 뒤 최소 adapter를 구현한다.
3. HDF5 도구와 public reader 결과를 모두 확인한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- global HDF5 handle을 만들지 마라. 이유: 수명과 오류 경계를 훼손한다.
- reference CSV 기능을 추가하지 마라. 이유: validation phase 책임이다.
- schema에 빈 미래 분석 결과를 넣지 마라. 이유: 현재 계약만 저장한다.
+56
View File
@@ -0,0 +1,56 @@
# Step 2: Linear Static Analysis
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/model/domain.hpp`
- `/include/fesa/fem/dof_manager.hpp`
- `/include/fesa/assembly/`
- `/include/fesa/constraints/`
- `/include/fesa/solvers/linear/`
- `/include/fesa/results/result_database.hpp`
## 작업
기존 production 모듈을 조율하는 `LinearStaticAnalysis` lifecycle을 구현한다.
```cpp
struct AnalysisRunResult final {
bool succeeded;
std::optional<ResultDatabase> results;
std::vector<Diagnostic> diagnostics;
};
class LinearStaticAnalysis final {
public:
[[nodiscard]] AnalysisRunResult run(const Domain&) const;
};
```
- parser나 HDF5를 호출하지 않고 이미 검증된 Domain을 입력받는다.
- DofManager, pattern/assembly, BC elimination, PARDISO, full reconstruction,
reaction recovery, nodal ResultDatabase 순서로 실행한다.
- hand-check 가능한 한 요소 Domain으로 변위, 반력, residual을 먼저 테스트한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "LinearStaticAnalysis|StaticEquilibrium" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. analysis test가 연결 누락으로 실패하는 것을 확인한다.
2. orchestration만 구현하고 수치 kernel을 복제하지 않는다.
3. \(Ku-f-r\) 평형과 finite result를 검사한다.
4. 전체 테스트와 index를 갱신한다.
## 금지사항
- CLI option parsing을 analysis에 넣지 마라. 이유: application 경계를 깨뜨린다.
- nonlinear loop나 여러 step을 추가하지 마라. 이유: Phase 1 범위 밖이다.
- test-only solver 경로를 만들지 마라. 이유: production pipeline을 검증해야 한다.
+60
View File
@@ -0,0 +1,60 @@
# Step 3: CLI Pipeline Integration
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/include/fesa/io/abaqus/parser.hpp`
- `/include/fesa/io/abaqus/semantic_mapper.hpp`
- `/include/fesa/analysis/linear_static_analysis.hpp`
- `/include/fesa/io/hdf5/writer.hpp`
- `/src/fesa/cli/main.cpp`
## 작업
flat minimal fixture를 입력부터 HDF5까지 실행하는 public application 경로를 연결한다.
```cpp
struct AnalysisRequest final {
std::filesystem::path input_path;
std::filesystem::path output_path;
};
[[nodiscard]] AnalysisRunResult run_solver(const AnalysisRequest&);
```
CLI 계약:
```text
fesa solve <model.inp> --output <results.h5>
fesa --version
```
- 먼저 `MinimalCantileverPipeline` 통합 테스트를 작성한다.
- test는 `run_solver` 또는 CLI와 public HDF5 reader만 사용한다.
- 성공 파일의 ID, finite displacement, reaction/equilibrium diagnostic과 schema path,
실패 입력의 nonzero exit 및 source diagnostic을 검증한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R MinimalCantileverPipeline --output-on-failure
.\out\build\windows-debug\Debug\fesa.exe solve tests\fixtures\abaqus\minimal_cantilever.inp --output out\minimal-cantilever.h5
h5ls -r out\minimal-cantilever.h5
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. end-to-end test 실패를 확인한다.
2. parser→Domain→analysis→writer만 조율한다.
3. command와 public reader로 산출물을 재검증한다.
4. 이 milestone을 수치 자격 완료로 표시하지 말고 index를 갱신한다.
## 금지사항
- hierarchical sample의 전체 keyword를 우회 처리하지 마라. 이유: 다음 input phase다.
- fake stiffness/result를 쓰지 마라. 이유: 실제 pipeline 검증을 무효화한다.
- CLI에 solver 내부 구현을 넣지 마라. 이유: core/library 재사용성을 훼손한다.
+21
View File
@@ -0,0 +1,21 @@
{
"project": "FESA",
"phase": "solver-bootstrap",
"steps": [
{
"step": 0,
"name": "cmake-project-scaffold",
"status": "pending"
},
{
"step": 1,
"name": "dependency-smoke-tests",
"status": "pending"
},
{
"step": 2,
"name": "core-ids-and-diagnostics",
"status": "pending"
}
]
}
+62
View File
@@ -0,0 +1,62 @@
# Step 0: CMake Project Scaffold
## 읽어야 할 파일
먼저 아래 파일을 모두 읽고 저장소 계약을 파악하라.
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/HARNESS.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/.harness/config.example.json`
## 작업
C++20/MSVC x64 프로젝트의 최소 실행 가능한 build/test 뼈대를 만든다.
- `CMakeLists.txt`, `CMakePresets.json`, `cmake/FesaDependencies.cmake`,
`.harness/config.json`, `tests/CMakeLists.txt`를 만든다.
- 실제 target은 `fesa_core` 정적 라이브러리와 `fesa` CLI 두 개만 만든다.
- `include/fesa/core/version.hpp`, `src/fesa/core/version.cpp`,
`src/fesa/cli/main.cpp`에 다음 계약을 구현한다.
```cpp
namespace fesa {
[[nodiscard]] std::string_view version() noexcept;
}
```
- CLI는 이 step에서 `fesa --version`만 처리한다.
- 먼저 `VersionCommand` CTest를 등록해 실패를 확인한 뒤 최소 구현한다.
- `windows-debug`, `windows-release` configure/build/test preset을 정의한다.
- build 산출물은 `out/build/<preset>` 아래에만 둔다.
- MSVC가 아니거나 x64가 아니면 configure 단계에서 명확히 실패시킨다.
## Acceptance Criteria
```powershell
uv run --with pytest python -m pytest -v -rs
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug -R VersionCommand --output-on-failure
ctest --preset windows-debug --output-on-failure
```
`fesa --version`은 비어 있지 않은 FESA 버전을 출력하고 종료 코드 0을 반환해야 한다.
CTest는 0개가 아니어야 한다.
## 검증 절차
1. 테스트가 구현 전 실패하는 것을 확인한다.
2. Acceptance Criteria 명령을 새로 실행한다.
3. MSVC/C++20/x64와 산출물 경로를 확인한다.
4. 성공 시 index의 step을 `completed`로 바꾸고 생성 파일을 summary에 기록한다.
5. 도구가 없으면 자동 설치하지 말고 `blocked`와 정확한 누락 항목을 기록한다.
## 금지사항
- 외부 패키지를 다운로드하지 마라. 이유: 사전 설치 의존성 정책을 위반한다.
- MKL, TBB, HDF5 기능을 구현하지 마라. 이유: 다음 step의 독립 범위다.
- 빈 미래 모듈을 만들지 마라. 이유: Phase 1 최소 실체화 원칙을 위반한다.
+55
View File
@@ -0,0 +1,55 @@
# Step 1: Dependency Smoke Tests
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/HARNESS.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/CMakeLists.txt`
- `/CMakePresets.json`
- `/cmake/FesaDependencies.cmake`
- `/.harness/config.json`
- `/tests/CMakeLists.txt`
이전 step의 CMake target과 preset을 그대로 확장하라.
## 작업
사전 설치된 oneMKL, oneTBB, HDF5 C API, GoogleTest/GoogleMock을 CMake imported
target으로 찾고 링크 계약을 검증한다.
- `tests/unit/dependencies/dependency_smoke_test.cpp`를 먼저 작성한다.
- 테스트는 MKL의 작은 vector 연산, TBB의 제한된 parallel loop, HDF5 임시 파일
생성·닫기, GoogleTest 실행을 확인한다.
- `FesaDependencies.cmake``MKL::MKL`, TBB imported target, HDF5 C target,
GoogleTest target을 제공해야 한다.
- oneMKL은 LP64, dynamic link, TBB threading 조합을 사용한다.
- runtime DLL 또는 architecture 불일치는 configure diagnostic으로 보고한다.
## Acceptance Criteria
```powershell
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug -R DependencySmoke --output-on-failure
ctest --preset windows-debug --output-on-failure
```
네 의존성을 실제 호출하는 smoke test가 통과해야 하며 새 MSVC 경고가 없어야 한다.
## 검증 절차
1. smoke test를 먼저 추가하고 link 또는 실행 실패를 확인한다.
2. dependency discovery와 target link만 최소 수정한다.
3. 전체 configure/build/test를 새로 실행한다.
4. 성공 시 정확한 imported target과 탐색 파일을 summary에 기록한다.
5. 패키지나 MSVC가 없으면 세 차례 임의 수정하지 말고 `blocked`로 종료한다.
## 금지사항
- FetchContent, vcpkg, Conan 또는 다운로드를 추가하지 마라. 이유: ADR-002 위반이다.
- vendor 절대경로를 public header에 노출하지 마라. 이유: backend 격리를 깨뜨린다.
- solver 기능을 구현하지 마라. 이유: 이 step은 build dependency 계약만 다룬다.
+72
View File
@@ -0,0 +1,72 @@
# Step 2: Core IDs and Diagnostics
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/CMakeLists.txt`
- `/tests/CMakeLists.txt`
- `/include/fesa/core/version.hpp`
## 작업
외부 라이브러리에 의존하지 않는 `core` 값 타입을 TDD로 구현한다.
- 생성 파일:
`include/fesa/core/entity_id.hpp`, `vec3.hpp`, `source_location.hpp`,
`diagnostic.hpp`, `status.hpp`와 대응 테스트
- 인터페이스:
```cpp
template<class Tag>
class EntityId final {
public:
explicit constexpr EntityId(std::int64_t value);
[[nodiscard]] constexpr std::int64_t value() const noexcept;
auto operator<=>(const EntityId&) const = default;
};
struct Vec3 final { double x; double y; double z; };
struct SourceLocation final {
std::filesystem::path file;
std::size_t line;
std::size_t column;
};
enum class DiagnosticStage { io, syntax, semantic, model, equation, solver, results, validation };
enum class Severity { warning, error };
struct Diagnostic final {
DiagnosticStage stage;
Severity severity;
std::string code;
std::string message;
std::optional<SourceLocation> source;
};
```
- typed ID의 잘못된 암시 변환, 음수 ID, nonfinite vector와 diagnostic source 보존을
실패 테스트로 먼저 고정한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Core|Diagnostic|EntityId" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. production header 전에 실패하는 GoogleTest를 작성한다.
2. 최소 값 타입만 구현한다.
3. focused test와 전체 CTest를 실행한다.
4. `core`가 MKL, TBB, HDF5, Abaqus header를 include하지 않는지 확인한다.
5. index와 summary를 갱신한다.
## 금지사항
- 단위 변환 시스템을 만들지 마라. 이유: FESA는 일관 단위계만 사용한다.
- 범용 reflection이나 serialization을 만들지 마라. 이유: 요구되지 않았다.
- equation ID를 정의하지 마라. 이유: `DofManager` 단계의 책임이다.
+2
View File
@@ -0,0 +1,2 @@
[pytest]
testpaths = tests
+102
View File
@@ -0,0 +1,102 @@
*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.
*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
@@ -0,0 +1,12 @@
Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3
1,0,0,-1.00E-32,0,1.00E-31,0
2,0,0,-2.87E-06,0,5.43E-06,0
3,0,0,-1.09E-05,0,1.03E-05,0
4,0,0,-2.35E-05,0,1.46E-05,0
5,0,0,-4.00E-05,0,1.83E-05,0
6,0,0,-6.01E-05,0,2.14E-05,0
7,0,0,-8.29E-05,0,2.40E-05,0
8,0,0,-1.08E-04,0,2.60E-05,0
9,0,0,-1.35E-04,0,2.74E-05,0
10,0,0,-1.63E-04,0,2.83E-05,0
11,0,0,-1.92E-04,0,2.86E-05,0
1 Node Label U-U1 U-U2 U-U3 UR-UR1 UR-UR2 UR-UR3
2 1 0 0 -1.00E-32 0 1.00E-31 0
3 2 0 0 -2.87E-06 0 5.43E-06 0
4 3 0 0 -1.09E-05 0 1.03E-05 0
5 4 0 0 -2.35E-05 0 1.46E-05 0
6 5 0 0 -4.00E-05 0 1.83E-05 0
7 6 0 0 -6.01E-05 0 2.14E-05 0
8 7 0 0 -8.29E-05 0 2.40E-05 0
9 8 0 0 -1.08E-04 0 2.60E-05 0
10 9 0 0 -1.35E-04 0 2.74E-05 0
11 10 0 0 -1.63E-04 0 2.83E-05 0
12 11 0 0 -1.92E-04 0 2.86E-05 0
@@ -0,0 +1,12 @@
Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3
1,0,0,1.00E+04,0,-1.00E+05,0
2,0,0,0,0,0,0
3,0,0,0,0,0,0
4,0,0,0,0,0,0
5,0,0,0,0,0,0
6,0,0,0,0,0,0
7,0,0,0,0,0,0
8,0,0,0,0,0,0
9,0,0,0,0,0,0
10,0,0,0,0,0,0
11,0,0,0,0,0,0
1 Node Label RF-RF1 RF-RF2 RF-RF3 RM-RM1 RM-RM2 RM-RM3
2 1 0 0 1.00E+04 0 -1.00E+05 0
3 2 0 0 0 0 0 0
4 3 0 0 0 0 0 0
5 4 0 0 0 0 0 0
6 5 0 0 0 0 0 0
7 6 0 0 0 0 0 0
8 7 0 0 0 0 0 0
9 8 0 0 0 0 0 0
10 9 0 0 0 0 0 0
11 10 0 0 0 0 0 0
12 11 0 0 0 0 0 0
@@ -0,0 +1,101 @@
*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
*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
*Nset, nset=Set-1, generate
1, 11, 1
*Elset, elset=Set-1, generate
1, 10, 1
*Nset, nset=Set-2, generate
1, 11, 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.
*End Part
**
**
** ASSEMBLY
**
*Assembly, name=Assembly
**
*Instance, name=Part-1-1, part=Part-1
*End Instance
**
*Nset, nset=Set-1, instance=Part-1-1
11,
*Nset, nset=Set-2, instance=Part-1-1
1,
*End Assembly
**
** MATERIALS
**
*Material, name=Material-1
*Elastic
2.1e+11, 0.3
** ----------------------------------------------------------------
**
** STEP: Step-1
**
*Step, name=Step-1, nlgeom=NO
*Static
1., 1., 1e-05, 1.
**
** BOUNDARY CONDITIONS
**
** Name: BC-1 Type: Displacement/Rotation
*Boundary
Set-2, 1, 1
Set-2, 2, 2
Set-2, 3, 3
Set-2, 4, 4
Set-2, 5, 5
Set-2, 6, 6
**
** LOADS
**
** Name: Load-1 Type: Concentrated force
*Cload
Set-1, 3, -10000.
**
** OUTPUT REQUESTS
**
*Restart, write, frequency=0
**
** FIELD OUTPUT: F-Output-1
**
*Output, field, variable=PRESELECT
**
** HISTORY OUTPUT: H-Output-1
**
*Output, history, variable=PRESELECT
*End Step
+481
View File
@@ -0,0 +1,481 @@
#!/usr/bin/env python3
"""
Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
Usage:
python scripts/execute.py <phase-dir> [--push]
"""
import argparse
import contextlib
import json
import os
import subprocess
import sys
import threading
import time
import types
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional
ROOT = Path(__file__).resolve().parent.parent
class CodexEnvironmentError(RuntimeError):
"""재시도로 해결할 수 없는 Codex CLI 환경 오류."""
@contextlib.contextmanager
def progress_indicator(label: str):
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
frames = "◐◓◑◒"
stop = threading.Event()
t0 = time.monotonic()
def _animate():
idx = 0
while not stop.wait(0.12):
sec = int(time.monotonic() - t0)
sys.stderr.write(f"\r{frames[idx % len(frames)]} {label} [{sec}s]")
sys.stderr.flush()
idx += 1
sys.stderr.write("\r" + " " * (len(label) + 20) + "\r")
sys.stderr.flush()
th = threading.Thread(target=_animate, daemon=True)
th.start()
info = types.SimpleNamespace(elapsed=0.0)
try:
yield info
finally:
stop.set()
th.join()
info.elapsed = time.monotonic() - t0
class StepExecutor:
"""Phase 디렉토리 안의 step들을 순차 실행하는 하네스."""
MAX_RETRIES = 3
FEAT_MSG = "feat({phase}): step {num}{name}"
CHORE_MSG = "chore({phase}): step {num} output"
TZ = timezone(timedelta(hours=9))
def __init__(self, phase_dir_name: str, *, auto_push: bool = False):
self._root = str(ROOT)
self._phases_dir = ROOT / "phases"
self._phase_dir = self._phases_dir / phase_dir_name
self._phase_dir_name = phase_dir_name
self._top_index_file = self._phases_dir / "index.json"
self._auto_push = auto_push
if not self._phase_dir.is_dir():
print(f"ERROR: {self._phase_dir} not found")
sys.exit(1)
self._index_file = self._phase_dir / "index.json"
if not self._index_file.exists():
print(f"ERROR: {self._index_file} not found")
sys.exit(1)
idx = self._read_json(self._index_file)
self._project = idx.get("project", "project")
self._phase_name = idx.get("phase", phase_dir_name)
self._total = len(idx["steps"])
def run(self):
self._print_header()
self._check_blockers()
self._checkout_branch()
guardrails = self._load_guardrails()
self._ensure_created_at()
self._execute_all_steps(guardrails)
self._finalize()
# --- timestamps ---
def _stamp(self) -> str:
return datetime.now(self.TZ).strftime("%Y-%m-%dT%H:%M:%S%z")
# --- JSON I/O ---
@staticmethod
def _read_json(p: Path) -> dict:
return json.loads(p.read_text(encoding="utf-8"))
@staticmethod
def _write_json(p: Path, data: dict):
p.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
# --- git ---
def _run_git(self, *args) -> subprocess.CompletedProcess:
cmd = ["git"] + list(args)
return subprocess.run(cmd, cwd=self._root, capture_output=True, text=True)
def _checkout_branch(self):
branch = f"feat-{self._phase_name}"
r = self._run_git("rev-parse", "--abbrev-ref", "HEAD")
if r.returncode != 0:
print(f" ERROR: git을 사용할 수 없거나 git repo가 아닙니다.")
print(f" {r.stderr.strip()}")
sys.exit(1)
if r.stdout.strip() == branch:
return
r = self._run_git("rev-parse", "--verify", branch)
r = self._run_git("checkout", branch) if r.returncode == 0 else self._run_git("checkout", "-b", branch)
if r.returncode != 0:
print(f" ERROR: 브랜치 '{branch}' checkout 실패.")
print(f" {r.stderr.strip()}")
print(f" Hint: 변경사항을 stash하거나 commit한 후 다시 시도하세요.")
sys.exit(1)
print(f" Branch: {branch}")
def _commit_step(self, step_num: int, step_name: str):
output_rel = f"phases/{self._phase_dir_name}/step{step_num}-output.json"
index_rel = f"phases/{self._phase_dir_name}/index.json"
self._run_git("add", "-A")
self._run_git("reset", "HEAD", "--", output_rel)
self._run_git("reset", "HEAD", "--", index_rel)
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
msg = self.FEAT_MSG.format(phase=self._phase_name, num=step_num, name=step_name)
r = self._run_git("commit", "-m", msg)
if r.returncode == 0:
print(f" Commit: {msg}")
else:
print(f" WARN: 코드 커밋 실패: {r.stderr.strip()}")
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)
r = self._run_git("commit", "-m", msg)
if r.returncode != 0:
print(f" WARN: housekeeping 커밋 실패: {r.stderr.strip()}")
# --- top-level index ---
def _update_top_index(self, status: str):
if not self._top_index_file.exists():
return
top = self._read_json(self._top_index_file)
ts = self._stamp()
for phase in top.get("phases", []):
if phase.get("dir") == self._phase_dir_name:
phase["status"] = status
ts_key = {"completed": "completed_at", "error": "failed_at", "blocked": "blocked_at"}.get(status)
if ts_key:
phase[ts_key] = ts
break
self._write_json(self._top_index_file, top)
# --- guardrails & context ---
def _load_guardrails(self) -> str:
sections = []
agents_md = ROOT / "AGENTS.md"
if agents_md.exists():
sections.append(
"## 프로젝트 규칙 (AGENTS.md)\n\n"
f"{agents_md.read_text(encoding='utf-8')}"
)
docs_dir = ROOT / "docs"
if docs_dir.is_dir():
for doc in sorted(docs_dir.glob("*.md")):
sections.append(
f"## {doc.stem}\n\n{doc.read_text(encoding='utf-8')}"
)
return "\n\n---\n\n".join(sections) if sections else ""
@staticmethod
def _build_step_context(index: dict) -> str:
lines = [
f"- Step {s['step']} ({s['name']}): {s['summary']}"
for s in index["steps"]
if s["status"] == "completed" and s.get("summary")
]
if not lines:
return ""
return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n"
def _build_preamble(self, guardrails: str, step_context: str,
prev_error: Optional[str] = None) -> str:
retry_section = ""
if prev_error:
retry_section = (
f"\n## ⚠ 이전 시도 실패 — 아래 에러를 반드시 참고하여 수정하라\n\n"
f"{prev_error}\n\n---\n\n"
)
return (
f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 step을 수행하세요.\n\n"
f"{guardrails}\n\n---\n\n"
f"{step_context}{retry_section}"
f"## 작업 규칙\n\n"
f"1. 이전 step에서 작성된 코드를 확인하고 일관성을 유지하라.\n"
f"2. 이 step에 명시된 작업만 수행하라. 추가 기능이나 파일을 만들지 마라.\n"
f"3. 기존 테스트를 깨뜨리지 마라.\n"
f"4. AC(Acceptance Criteria) 검증을 직접 실행하라.\n"
f"5. /phases/{self._phase_dir_name}/index.json의 해당 step status를 업데이트하라:\n"
f" - AC 통과 → \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n"
f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 → \"error\" + \"error_message\" 기록\n"
f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n"
f"6. 변경사항을 직접 커밋하지 마라. Git 커밋과 timestamp는 실행기가 처리한다.\n\n"
f"---\n\n"
)
# --- Codex 호출 ---
def _invoke_codex(self, step: dict, preamble: str) -> dict:
step_num, step_name = step["step"], step["name"]
step_file = self._phase_dir / f"step{step_num}.md"
if not step_file.exists():
print(f" ERROR: {step_file} not found")
sys.exit(1)
prompt = preamble + step_file.read_text(encoding="utf-8")
command = [
"codex",
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
"--cd",
self._root,
"-",
]
try:
result = subprocess.run(
command,
cwd=self._root,
input=prompt,
capture_output=True,
text=True,
timeout=1800,
)
except FileNotFoundError as exc:
raise CodexEnvironmentError(
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
) from exc
if result.returncode != 0:
print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})")
if result.stderr:
print(f" stderr: {result.stderr[:500]}")
output = {
"step": step_num, "name": step_name,
"exitCode": result.returncode,
"stdout": result.stdout, "stderr": result.stderr,
}
out_path = self._phase_dir / f"step{step_num}-output.json"
self._write_json(out_path, output)
return output
@staticmethod
def _codex_environment_failure(output: dict) -> Optional[str]:
if output.get("exitCode", 0) == 0:
return None
diagnostic = (
f"{output.get('stderr', '')}\n{output.get('stdout', '')}"
).lower()
markers = {
"not logged in": "Codex 인증이 필요합니다.",
"authentication": "Codex 인증에 실패했습니다.",
"unexpected argument": "현재 Codex CLI가 필요한 옵션을 지원하지 않습니다.",
"unrecognized option": "현재 Codex CLI가 필요한 옵션을 지원하지 않습니다.",
}
for marker, message in markers.items():
if marker in diagnostic:
return message
return None
# --- 헤더 & 검증 ---
def _print_header(self):
print(f"\n{'='*60}")
print(f" Harness Step Executor")
print(f" Phase: {self._phase_name} | Steps: {self._total}")
if self._auto_push:
print(f" Auto-push: enabled")
print(f"{'='*60}")
def _check_blockers(self):
index = self._read_json(self._index_file)
for s in reversed(index["steps"]):
if s["status"] == "error":
print(f"\n ✗ Step {s['step']} ({s['name']}) failed.")
print(f" Error: {s.get('error_message', 'unknown')}")
print(f" Fix and reset status to 'pending' to retry.")
sys.exit(1)
if s["status"] == "blocked":
print(f"\n ⏸ Step {s['step']} ({s['name']}) blocked.")
print(f" Reason: {s.get('blocked_reason', 'unknown')}")
print(f" Resolve and reset status to 'pending' to retry.")
sys.exit(2)
if s["status"] != "pending":
break
def _ensure_created_at(self):
index = self._read_json(self._index_file)
if "created_at" not in index:
index["created_at"] = self._stamp()
self._write_json(self._index_file, index)
# --- 실행 루프 ---
def _execute_single_step(self, step: dict, guardrails: str) -> bool:
"""단일 step 실행 (재시도 포함). 완료되면 True, 실패/차단이면 False."""
step_num, step_name = step["step"], step["name"]
done = sum(1 for s in self._read_json(self._index_file)["steps"] if s["status"] == "completed")
prev_error = None
for attempt in range(1, self.MAX_RETRIES + 1):
index = self._read_json(self._index_file)
step_context = self._build_step_context(index)
preamble = self._build_preamble(guardrails, step_context, prev_error)
tag = f"Step {step_num}/{self._total - 1} ({done} done): {step_name}"
if attempt > 1:
tag += f" [retry {attempt}/{self.MAX_RETRIES}]"
with progress_indicator(tag) as pi:
output = self._invoke_codex(step, preamble)
elapsed = int(pi.elapsed)
environment_failure = self._codex_environment_failure(output)
if environment_failure:
raise CodexEnvironmentError(environment_failure)
index = self._read_json(self._index_file)
status = next((s.get("status", "pending") for s in index["steps"] if s["step"] == step_num), "pending")
ts = self._stamp()
if status == "completed":
for s in index["steps"]:
if s["step"] == step_num:
s["completed_at"] = ts
self._write_json(self._index_file, index)
self._commit_step(step_num, step_name)
print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]")
return True
if status == "blocked":
for s in index["steps"]:
if s["step"] == step_num:
s["blocked_at"] = ts
self._write_json(self._index_file, index)
reason = next((s.get("blocked_reason", "") for s in index["steps"] if s["step"] == step_num), "")
print(f" ⏸ Step {step_num}: {step_name} blocked [{elapsed}s]")
print(f" Reason: {reason}")
self._update_top_index("blocked")
sys.exit(2)
runtime_error = ""
if output["exitCode"] != 0:
runtime_error = (
output.get("stderr", "").strip()
or output.get("stdout", "").strip()
)
err_msg = next(
(
s.get(
"error_message",
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:
for s in index["steps"]:
if s["step"] == step_num:
s["status"] = "pending"
s.pop("error_message", None)
self._write_json(self._index_file, index)
prev_error = err_msg
print(f" ↻ Step {step_num}: retry {attempt}/{self.MAX_RETRIES}{err_msg}")
else:
for s in index["steps"]:
if s["step"] == step_num:
s["status"] = "error"
s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}"
s["failed_at"] = ts
self._write_json(self._index_file, index)
self._commit_step(step_num, step_name)
print(f" ✗ Step {step_num}: {step_name} failed after {self.MAX_RETRIES} attempts [{elapsed}s]")
print(f" Error: {err_msg}")
self._update_top_index("error")
sys.exit(1)
return False # unreachable
def _execute_all_steps(self, guardrails: str):
while True:
index = self._read_json(self._index_file)
pending = next((s for s in index["steps"] if s["status"] == "pending"), None)
if pending is None:
print("\n All steps completed!")
return
step_num = pending["step"]
for s in index["steps"]:
if s["step"] == step_num and "started_at" not in s:
s["started_at"] = self._stamp()
self._write_json(self._index_file, index)
break
self._execute_single_step(pending, guardrails)
def _finalize(self):
index = self._read_json(self._index_file)
index["completed_at"] = self._stamp()
self._write_json(self._index_file, index)
self._update_top_index("completed")
self._run_git("add", "-A")
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
msg = f"chore({self._phase_name}): mark phase completed"
r = self._run_git("commit", "-m", msg)
if r.returncode == 0:
print(f"{msg}")
if self._auto_push:
branch = f"feat-{self._phase_name}"
r = self._run_git("push", "-u", "origin", branch)
if r.returncode != 0:
print(f"\n ERROR: git push 실패: {r.stderr.strip()}")
sys.exit(1)
print(f" ✓ Pushed to origin/{branch}")
print(f"\n{'='*60}")
print(f" Phase '{self._phase_name}' completed!")
print(f"{'='*60}")
def main():
parser = argparse.ArgumentParser(description="Harness Step Executor")
parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)")
parser.add_argument("--push", action="store_true", help="Push branch after completion")
args = parser.parse_args()
try:
StepExecutor(args.phase_dir, auto_push=args.push).run()
except CodexEnvironmentError as exc:
print(f"ERROR: {exc}")
sys.exit(1)
if __name__ == "__main__":
main()
+118
View File
@@ -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())
+148
View File
@@ -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())
+3
View File
@@ -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"]
+19
View File
@@ -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
+99
View File
@@ -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")),
),
)
+60
View File
@@ -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))
+146
View File
@@ -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),
)
+77
View File
@@ -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, ())
+100
View File
@@ -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
+140
View File
@@ -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)
+88
View File
@@ -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
+169
View File
@@ -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)