add uncommitted files
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
---
|
||||
name: harness
|
||||
description: Use when planning agentic implementation phases, creating phases/index.json and self-contained step files, or running the Harness step executor.
|
||||
---
|
||||
|
||||
# Harness Workflow
|
||||
|
||||
이 프로젝트는 Harness 프레임워크를 사용한다. 아래 워크플로에 따라 작업한다.
|
||||
|
||||
## A. 탐색
|
||||
|
||||
`AGENTS.md`와 `docs/` 하위 문서(PRD, ARCHITECTURE, ADR 등)를 읽고 프로젝트의 기획,
|
||||
아키텍처, 설계 의도를 파악한다. 병렬 탐색이 실제로 유용하고 현재 세션에서 허용될
|
||||
때만 Codex subagent를 선택적으로 사용한다.
|
||||
|
||||
## B. 논의
|
||||
|
||||
구현을 위해 구체화하거나 기술적으로 결정해야 할 사항이 있으면 사용자에게 한 번에
|
||||
하나씩 제시하고 논의한다.
|
||||
|
||||
## C. Step 설계
|
||||
|
||||
사용자가 구현 계획 작성을 지시하면 여러 step으로 나뉜 초안을 작성해 피드백을
|
||||
요청한다.
|
||||
|
||||
설계 원칙:
|
||||
|
||||
1. **Scope 최소화** — 하나의 step에서 하나의 레이어 또는 모듈만 다룬다. 여러
|
||||
모듈을 동시에 수정해야 하면 step을 쪼갠다.
|
||||
2. **자기완결성** — 각 step 파일은 독립된 Codex 실행에서 사용된다. 외부 대화
|
||||
참조를 금지하고 필요한 정보를 모두 파일 안에 적는다.
|
||||
3. **사전 준비 강제** — 관련 문서와 이전 step에서 생성하거나 수정한 파일 경로를
|
||||
명시한다.
|
||||
4. **시그니처 수준 지시** — 함수와 클래스의 인터페이스를 제시하고 내부 구현은
|
||||
Codex 재량에 맡긴다. 멱등성, 보안, 데이터 무결성 같은 핵심 규칙은 명시한다.
|
||||
5. **AC는 실행 가능한 command** — 추상적 조건 대신 실제 빌드와 테스트 command를
|
||||
포함한다.
|
||||
6. **주의사항은 구체적으로** — "X를 하지 마라. 이유: Y" 형식으로 적는다.
|
||||
7. **네이밍** — step name은 핵심 작업을 표현하는 kebab-case slug로 정한다.
|
||||
|
||||
## D. 파일 생성
|
||||
|
||||
사용자가 초안을 승인한 후에만 다음 파일을 생성한다.
|
||||
|
||||
### D-1. `phases/index.json`
|
||||
|
||||
여러 task를 관리하는 top-level 인덱스다. 이미 존재하면 `phases` 배열에 새 항목을
|
||||
추가한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"phases": [
|
||||
{
|
||||
"dir": "0-mvp",
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `dir`: task 디렉터리명
|
||||
- `status`: `pending` | `completed` | `error` | `blocked`
|
||||
- timestamp는 executor가 상태를 바꿀 때 기록하므로 생성 시 넣지 않는다.
|
||||
|
||||
### D-2. `phases/{task-name}/index.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "<프로젝트명>",
|
||||
"phase": "<task-name>",
|
||||
"steps": [
|
||||
{ "step": 0, "name": "project-setup", "status": "pending" },
|
||||
{ "step": 1, "name": "core-types", "status": "pending" },
|
||||
{ "step": 2, "name": "api-layer", "status": "pending" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
필드 규칙:
|
||||
|
||||
- `project`: `AGENTS.md`에 정의된 프로젝트명
|
||||
- `phase`: task 이름이며 디렉터리명과 일치
|
||||
- `steps[].step`: 0부터 시작하는 순번
|
||||
- `steps[].name`: kebab-case slug
|
||||
- `steps[].status`: 초기값 `pending`
|
||||
|
||||
상태와 기록 주체:
|
||||
|
||||
| 전이 | 기록 필드 | 기록 주체 |
|
||||
|------|-----------|-----------|
|
||||
| `completed` | `summary`, `completed_at` | Codex가 summary, executor가 timestamp |
|
||||
| `error` | `error_message`, `failed_at` | Codex가 message, executor가 timestamp |
|
||||
| `blocked` | `blocked_reason`, `blocked_at` | Codex가 reason, executor가 timestamp |
|
||||
|
||||
`summary`에는 다음 step에 유용한 생성 파일과 핵심 결정을 한 줄로 적는다.
|
||||
task `created_at`과 step `started_at`은 executor가 기록하므로 생성 시 넣지 않는다.
|
||||
|
||||
### D-3. `phases/{task-name}/step{N}.md`
|
||||
|
||||
````markdown
|
||||
# Step {N}: {이름}
|
||||
|
||||
## 읽어야 할 파일
|
||||
|
||||
먼저 아래 파일을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라:
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- 이전 step에서 생성하거나 수정한 파일 경로
|
||||
|
||||
이전 step의 코드를 꼼꼼히 읽고 설계 의도를 이해한 뒤 작업하라.
|
||||
|
||||
## 작업
|
||||
|
||||
구체적인 구현 지시를 파일 경로, 클래스와 함수 시그니처, 로직 설명과 함께 적는다.
|
||||
구현체는 Codex에 맡기되 설계 의도에서 벗어나면 안 되는 핵심 규칙은 명시한다.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
프로젝트 형식에 맞는 명령을 사용한다. `.harness/config.json`이 있으면 해당 preset,
|
||||
solution, configuration, platform, test command를 우선한다.
|
||||
|
||||
```powershell
|
||||
# CMake
|
||||
cmake --build .harness/build --config Debug
|
||||
ctest --test-dir .harness/build -C Debug --output-on-failure
|
||||
|
||||
# 직접 MSBuild
|
||||
MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64
|
||||
.\build\tests\Debug\MyProjectTests.exe
|
||||
```
|
||||
|
||||
## 검증 절차
|
||||
|
||||
1. Acceptance Criteria command를 실행한다.
|
||||
2. ARCHITECTURE 디렉터리 구조를 따르는지 확인한다.
|
||||
3. ADR 기술 스택과 `AGENTS.md` CRITICAL 규칙을 확인한다.
|
||||
4. 결과에 따라 task index의 해당 step을 갱신한다.
|
||||
- 성공: `status`를 `completed`로 바꾸고 한 줄 `summary` 기록
|
||||
- 수정 3회 후 실패: `status`를 `error`로 바꾸고 `error_message` 기록
|
||||
- 사용자 개입 필요: `status`를 `blocked`로 바꾸고 `blocked_reason` 기록 후 중단
|
||||
|
||||
## 금지사항
|
||||
|
||||
- 이 step의 범위 밖 기능을 추가하지 마라. 이유: step의 독립성을 깨뜨린다.
|
||||
- 기존 테스트를 깨뜨리지 마라. 이유: 이전 동작을 회귀시킨다.
|
||||
````
|
||||
|
||||
## E. 실행
|
||||
|
||||
```bash
|
||||
python scripts/execute.py {task-name}
|
||||
python scripts/execute.py {task-name} --push
|
||||
```
|
||||
|
||||
환경에서 Python 3 실행 명령이 `python3`이면 그 명령을 대신 사용한다.
|
||||
|
||||
executor가 처리하는 작업:
|
||||
|
||||
- `feat-{task-name}` 브랜치 생성 또는 checkout
|
||||
- `AGENTS.md`와 `docs/*.md` guardrail 주입
|
||||
- 완료 step의 summary를 다음 prompt에 누적
|
||||
- 실패 시 최대 3회 재시도하며 이전 오류를 prompt에 전달
|
||||
- 코드 변경과 metadata를 분리해 commit
|
||||
- `started_at`, `completed_at`, `failed_at`, `blocked_at` 기록
|
||||
|
||||
에러 복구:
|
||||
|
||||
- `error`: 해당 status를 `pending`으로 바꾸고 `error_message`를 삭제한 뒤 재실행
|
||||
- `blocked`: 원인을 해결하고 status를 `pending`으로 바꾸고 `blocked_reason`을 삭제한
|
||||
뒤 재실행
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: review
|
||||
description: Use when reviewing repository changes against AGENTS.md, architecture decisions, tests, and build requirements.
|
||||
---
|
||||
|
||||
# Repository Review
|
||||
|
||||
먼저 다음 문서를 읽는다.
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
|
||||
사용자가 범위를 지정하지 않으면 현재 작업 트리의 변경을 리뷰한다. 관련 diff를
|
||||
확인하고 가능한 빌드와 테스트 command를 실제로 실행한다.
|
||||
|
||||
## 체크리스트
|
||||
|
||||
1. MSVC toolset와 C++ 표준이 AGENTS.md/ADR과 일치하는가?
|
||||
2. CMake 또는 MSBuild Debug/x64 빌드가 통과하는가?
|
||||
3. CTest 또는 `.harness/config.json`의 명시적 test command가 통과하는가?
|
||||
4. 새 C/C++ 소스와 헤더에 대응 테스트가 있는가?
|
||||
5. CRITICAL 아키텍처 규칙과 public header 경계를 지키는가?
|
||||
|
||||
## 출력 형식
|
||||
|
||||
실제 결함을 심각도순으로 먼저 제시한다. 각 finding에 파일과 줄 번호, 영향,
|
||||
재현 또는 근거, 구체적인 수정 방안을 포함한다.
|
||||
|
||||
그 뒤 다음 표를 제공한다.
|
||||
|
||||
| 항목 | 결과 | 비고 |
|
||||
|------|------|------|
|
||||
| 아키텍처 준수 | ✅/❌/미검증 | 상세 |
|
||||
| 기술 스택 준수 | ✅/❌/미검증 | 상세 |
|
||||
| 테스트 존재 | ✅/❌/미검증 | 상세 |
|
||||
| CRITICAL 규칙 | ✅/❌/미검증 | 상세 |
|
||||
| 빌드 가능 | ✅/❌/미검증 | 상세 |
|
||||
|
||||
실행할 수 없는 검사는 성공으로 추정하지 말고 `미검증`과 이유를 적는다. finding이
|
||||
없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다.
|
||||
@@ -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
@@ -1 +1,5 @@
|
||||
.vs/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.py[cod]
|
||||
.harness/build/
|
||||
.worktrees/
|
||||
|
||||
@@ -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/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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`에서 관리한다.
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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를 보존해야 한다.
|
||||
@@ -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 범위다.
|
||||
@@ -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을 만들지 마라. 이유: 실제 두 번째 구현이 없다.
|
||||
@@ -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 입력 계약이다.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 책임이다.
|
||||
@@ -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를 추가하지 마라. 이유: 해석 중 모델 불변성을 깨뜨린다.
|
||||
@@ -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 정보로 보존한다.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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에 복제하지 마라. 이유: 모듈 경계를 깨뜨린다.
|
||||
@@ -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를 추가하지 마라. 이유: 범위 밖이다.
|
||||
@@ -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 정책 위반이다.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 오류를 숨긴다.
|
||||
@@ -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 범위 밖이다.
|
||||
@@ -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의 책임이다.
|
||||
@@ -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 밖이다.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 파일을 다시 요구하지 마라. 이유: 승인된 검증 계약과 다르다.
|
||||
@@ -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 불일치 위험이 있다.
|
||||
@@ -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만 제공한다.
|
||||
@@ -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에 묶지 마라. 이유: 머신 성능에 따라 달라진다.
|
||||
- 결과 정확성 검사를 생략하지 마라. 이유: 빠른 오답은 성능 증거가 아니다.
|
||||
- 실제 측정 없이 목표 시간을 만들지 마라. 이유: 요구사항에 근거가 없다.
|
||||
@@ -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 호환성을 잃는다.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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에서만 실체화한다.
|
||||
@@ -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에 빈 미래 분석 결과를 넣지 마라. 이유: 현재 계약만 저장한다.
|
||||
@@ -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을 검증해야 한다.
|
||||
@@ -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 재사용성을 훼손한다.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 최소 실체화 원칙을 위반한다.
|
||||
@@ -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 계약만 다룬다.
|
||||
@@ -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` 단계의 책임이다.
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Codex PreToolUse 정책: 위험 명령과 테스트 없는 구현 파일 수정을 차단한다."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
from msvc_harness.config import ConfigError, load_config
|
||||
from msvc_harness.tdd_policy import CPP_SUFFIXES, evaluate_paths
|
||||
|
||||
|
||||
DANGEROUS_PATTERNS = (
|
||||
re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE),
|
||||
re.compile(r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b", re.IGNORECASE),
|
||||
re.compile(r"\brm\s+-rf\b", re.IGNORECASE),
|
||||
re.compile(
|
||||
r"\bRemove-Item\b(?=[^\n]*-Recurse\b)(?=[^\n]*-Force\b)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
re.compile(
|
||||
r"\b(?:rmdir|rd)\b(?=[^\n]*/s\b)(?=[^\n]*/q\b)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
re.compile(r"\bDROP\s+TABLE\b", re.IGNORECASE),
|
||||
)
|
||||
PATCH_PATH = re.compile(
|
||||
r"^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:) (?P<path>.+?)\s*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def _tool_input(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
value = payload.get("tool_input", {})
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _candidate_paths(payload: dict[str, Any], root: Path) -> list[Path]:
|
||||
tool_input = _tool_input(payload)
|
||||
raw_paths: list[str] = []
|
||||
for key in ("path", "file_path"):
|
||||
value = tool_input.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
raw_paths.append(value.strip())
|
||||
|
||||
edits = tool_input.get("edits")
|
||||
if isinstance(edits, list):
|
||||
for edit in edits:
|
||||
if not isinstance(edit, dict):
|
||||
continue
|
||||
value = edit.get("path")
|
||||
if isinstance(value, str) and value.strip():
|
||||
raw_paths.append(value.strip())
|
||||
|
||||
for key in ("patch", "input"):
|
||||
value = tool_input.get(key)
|
||||
if isinstance(value, str):
|
||||
raw_paths.extend(match.group("path") for match in PATCH_PATH.finditer(value))
|
||||
|
||||
paths: list[Path] = []
|
||||
for raw_path in raw_paths:
|
||||
path = Path(raw_path)
|
||||
paths.append(path.resolve() if path.is_absolute() else (root / path).resolve())
|
||||
return list(dict.fromkeys(paths))
|
||||
|
||||
|
||||
def evaluate(payload: dict[str, Any], root: Path) -> str | None:
|
||||
"""Return a blocking reason, or None when the tool call is allowed."""
|
||||
tool_name = str(payload.get("tool_name", ""))
|
||||
tool_input = _tool_input(payload)
|
||||
|
||||
if tool_name in {"Bash", "shell_command", "PowerShell"}:
|
||||
command = tool_input.get("command", "")
|
||||
if isinstance(command, str) and any(
|
||||
pattern.search(command) for pattern in DANGEROUS_PATTERNS
|
||||
):
|
||||
return "위험한 명령어가 감지되어 실행을 차단했습니다."
|
||||
|
||||
if tool_name not in {"apply_patch", "Edit", "MultiEdit", "Write"}:
|
||||
return None
|
||||
|
||||
paths = _candidate_paths(payload, root)
|
||||
try:
|
||||
config = load_config(root)
|
||||
except ConfigError as exc:
|
||||
if any(path.suffix.lower() in CPP_SUFFIXES for path in paths):
|
||||
return f"TDD GUARD: .harness/config.json must be repaired: {exc}"
|
||||
return None
|
||||
return evaluate_paths(paths, root, config.tdd)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
if not isinstance(payload, dict):
|
||||
raise TypeError("hook input must be a JSON object")
|
||||
cwd = payload.get("cwd")
|
||||
if cwd is not None and not isinstance(cwd, str):
|
||||
raise TypeError("hook cwd must be a string")
|
||||
except (json.JSONDecodeError, TypeError) as exc:
|
||||
print(f"TDD GUARD: hook 입력을 해석하지 못해 검사를 건너뜁니다: {exc}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
root = Path(payload.get("cwd") or Path.cwd()).resolve()
|
||||
reason = evaluate(payload, root)
|
||||
if reason:
|
||||
print(reason, file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Codex Stop hook: validate discovered C/C++ projects with MSVC."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPTS = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from msvc_harness.adapters.base import AdapterError
|
||||
from msvc_harness.adapters.cmake import CMakeAdapter
|
||||
from msvc_harness.adapters.msbuild import MSBuildAdapter
|
||||
from msvc_harness.config import ConfigError, load_config
|
||||
from msvc_harness.discovery import DiscoveryError, discover_project
|
||||
from msvc_harness.models import ProjectKind
|
||||
from msvc_harness.process import ValidationFailure, execute_plan
|
||||
from msvc_harness.toolchain import ToolchainError, discover_toolchain
|
||||
|
||||
|
||||
REENTRY_ENV = "CODEX_STOP_VALIDATION_ACTIVE"
|
||||
TOTAL_TIMEOUT_SECONDS = 1800
|
||||
|
||||
|
||||
def _remaining(deadline, clock, stage):
|
||||
remaining = deadline - clock()
|
||||
if remaining <= 0:
|
||||
raise ValidationFailure(f"{stage} timed out before it could start")
|
||||
return remaining
|
||||
|
||||
|
||||
def _project_root(
|
||||
cwd: Path,
|
||||
*,
|
||||
deadline: float,
|
||||
run=subprocess.run,
|
||||
clock=time.monotonic,
|
||||
) -> Path:
|
||||
argv = ["git", "rev-parse", "--show-toplevel"]
|
||||
resolved_cwd = cwd.resolve()
|
||||
try:
|
||||
result = run(
|
||||
argv,
|
||||
cwd=resolved_cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
shell=False,
|
||||
timeout=_remaining(deadline, clock, "repository discovery"),
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ValidationFailure(
|
||||
f"repository discovery timed out; argv={argv!r}; "
|
||||
f"cwd={str(resolved_cwd)!r}"
|
||||
) from exc
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return Path(result.stdout.strip()).resolve()
|
||||
return resolved_cwd
|
||||
|
||||
|
||||
def run_validations(
|
||||
root: Path,
|
||||
*,
|
||||
deadline: float | None = None,
|
||||
clock=time.monotonic,
|
||||
) -> tuple[bool, str]:
|
||||
"""Build and test a discovered C/C++ project, when one exists."""
|
||||
if deadline is None:
|
||||
deadline = clock() + TOTAL_TIMEOUT_SECONDS
|
||||
try:
|
||||
config = load_config(root)
|
||||
discovery = discover_project(root, config)
|
||||
if discovery.selection is None:
|
||||
return True, ""
|
||||
selection = discovery.selection
|
||||
tools = discover_toolchain(
|
||||
selection.kind,
|
||||
deadline=deadline,
|
||||
clock=clock,
|
||||
)
|
||||
adapter = (
|
||||
CMakeAdapter()
|
||||
if selection.kind is ProjectKind.CMAKE
|
||||
else MSBuildAdapter()
|
||||
)
|
||||
plan = adapter.create_plan(root, selection, config, tools)
|
||||
child_env = os.environ.copy()
|
||||
child_env[REENTRY_ENV] = "1"
|
||||
execute_plan(
|
||||
plan,
|
||||
root,
|
||||
env=child_env,
|
||||
deadline=deadline,
|
||||
clock=clock,
|
||||
)
|
||||
return True, ""
|
||||
except (
|
||||
ConfigError,
|
||||
DiscoveryError,
|
||||
ToolchainError,
|
||||
AdapterError,
|
||||
ValidationFailure,
|
||||
OSError,
|
||||
) as exc:
|
||||
return False, str(exc)
|
||||
|
||||
|
||||
def _emit_stop_response(message: str) -> None:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"continue": False,
|
||||
"stopReason": message,
|
||||
"systemMessage": message,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main(*, run=subprocess.run, clock=time.monotonic) -> int:
|
||||
if os.environ.get(REENTRY_ENV) == "1":
|
||||
return 0
|
||||
|
||||
deadline = clock() + TOTAL_TIMEOUT_SECONDS
|
||||
try:
|
||||
root = _project_root(
|
||||
Path.cwd(),
|
||||
deadline=deadline,
|
||||
run=run,
|
||||
clock=clock,
|
||||
)
|
||||
ok, message = run_validations(root, deadline=deadline, clock=clock)
|
||||
except (OSError, ValidationFailure) as exc:
|
||||
_emit_stop_response(f"validation hook failed: {exc}")
|
||||
return 0
|
||||
|
||||
if ok:
|
||||
return 0
|
||||
_emit_stop_response(message)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
from .config import ConfigError, load_config
|
||||
|
||||
__all__ = ["ConfigError", "load_config"]
|
||||
@@ -0,0 +1,5 @@
|
||||
from .base import AdapterError, ValidationAdapter
|
||||
from .cmake import CMakeAdapter
|
||||
from .msbuild import MSBuildAdapter
|
||||
|
||||
__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"]
|
||||
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from ..models import HarnessConfig, ProjectSelection, Toolchain, ValidationPlan
|
||||
|
||||
|
||||
class AdapterError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class ValidationAdapter(Protocol):
|
||||
def create_plan(
|
||||
self,
|
||||
root: Path,
|
||||
selection: ProjectSelection,
|
||||
config: HarnessConfig,
|
||||
tools: Toolchain,
|
||||
) -> ValidationPlan:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,99 @@
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CommandSpec,
|
||||
HarnessConfig,
|
||||
ProjectKind,
|
||||
ProjectSelection,
|
||||
ResultCheck,
|
||||
ResultCheckKind,
|
||||
Toolchain,
|
||||
ValidationPlan,
|
||||
ValidationStep,
|
||||
)
|
||||
from .base import AdapterError
|
||||
|
||||
|
||||
class CMakeAdapter:
|
||||
def create_plan(
|
||||
self,
|
||||
root: Path,
|
||||
selection: ProjectSelection,
|
||||
config: HarnessConfig,
|
||||
tools: Toolchain,
|
||||
) -> ValidationPlan:
|
||||
if selection.kind is not ProjectKind.CMAKE:
|
||||
raise AdapterError("CMakeAdapter requires a CMake project selection")
|
||||
if tools.cmake is None:
|
||||
raise AdapterError("CMake tool is required")
|
||||
if tools.ctest is None:
|
||||
raise AdapterError("CTest tool is required")
|
||||
|
||||
if config.cmake.configure_preset is None:
|
||||
command_cwd = root
|
||||
binary = root / ".harness/build"
|
||||
configure = (
|
||||
str(tools.cmake),
|
||||
"-S",
|
||||
str(config.cmake.source_dir),
|
||||
"-B",
|
||||
str(binary),
|
||||
"-A",
|
||||
"x64",
|
||||
)
|
||||
build = (str(tools.cmake), "--build", str(binary), "--config", "Debug")
|
||||
discover = (
|
||||
str(tools.ctest),
|
||||
"--test-dir",
|
||||
str(binary),
|
||||
"-C",
|
||||
"Debug",
|
||||
"--show-only=json-v1",
|
||||
)
|
||||
test = (
|
||||
str(tools.ctest),
|
||||
"--test-dir",
|
||||
str(binary),
|
||||
"-C",
|
||||
"Debug",
|
||||
"--output-on-failure",
|
||||
)
|
||||
else:
|
||||
command_cwd = config.cmake.source_dir
|
||||
binary = config.cmake.binary_dir
|
||||
if (
|
||||
binary is None
|
||||
or config.cmake.build_preset is None
|
||||
or config.cmake.test_preset is None
|
||||
):
|
||||
raise AdapterError("CMake presets require binary, build, and test settings")
|
||||
configure = (str(tools.cmake), "--preset", config.cmake.configure_preset)
|
||||
build = (str(tools.cmake), "--build", "--preset", config.cmake.build_preset)
|
||||
discover = (
|
||||
str(tools.ctest),
|
||||
"--preset",
|
||||
config.cmake.test_preset,
|
||||
"--show-only=json-v1",
|
||||
)
|
||||
test = (
|
||||
str(tools.ctest),
|
||||
"--preset",
|
||||
config.cmake.test_preset,
|
||||
"--output-on-failure",
|
||||
)
|
||||
|
||||
return ValidationPlan(
|
||||
ProjectKind.CMAKE,
|
||||
(
|
||||
ValidationStep(
|
||||
CommandSpec(configure, command_cwd, "configure"),
|
||||
(ResultCheck(ResultCheckKind.CMAKE_COMPILER_IS_MSVC, binary),),
|
||||
),
|
||||
ValidationStep(CommandSpec(build, command_cwd, "build")),
|
||||
ValidationStep(
|
||||
CommandSpec(discover, command_cwd, "test-discovery"),
|
||||
(ResultCheck(ResultCheckKind.CTEST_HAS_TESTS),),
|
||||
),
|
||||
ValidationStep(CommandSpec(test, command_cwd, "test")),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CommandSpec,
|
||||
HarnessConfig,
|
||||
ProjectKind,
|
||||
ProjectSelection,
|
||||
Toolchain,
|
||||
ValidationPlan,
|
||||
ValidationStep,
|
||||
)
|
||||
from .base import AdapterError
|
||||
|
||||
|
||||
def _test_argv(root: Path, command: tuple[str, ...]) -> tuple[str, ...]:
|
||||
if "/" not in command[0] and "\\" not in command[0]:
|
||||
return command
|
||||
first = Path(command[0].replace("\\", "/"))
|
||||
executable = first if first.is_absolute() else (root / first).resolve()
|
||||
try:
|
||||
executable.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise AdapterError(
|
||||
"msbuild.testCommand resolves outside the repository"
|
||||
) from exc
|
||||
return (str(executable), *command[1:])
|
||||
|
||||
|
||||
class MSBuildAdapter:
|
||||
def create_plan(
|
||||
self,
|
||||
root: Path,
|
||||
selection: ProjectSelection,
|
||||
config: HarnessConfig,
|
||||
tools: Toolchain,
|
||||
) -> ValidationPlan:
|
||||
if selection.kind is not ProjectKind.MSBUILD:
|
||||
raise AdapterError("MSBuildAdapter received a non-MSBuild project")
|
||||
if not config.msbuild.test_command:
|
||||
raise AdapterError(
|
||||
"msbuild.testCommand is required for .sln/.vcxproj validation"
|
||||
)
|
||||
build = ValidationStep(
|
||||
CommandSpec(
|
||||
(
|
||||
str(tools.msbuild),
|
||||
str(selection.project_file),
|
||||
"/m",
|
||||
"/nologo",
|
||||
f"/p:Configuration={config.msbuild.configuration}",
|
||||
f"/p:Platform={config.msbuild.platform}",
|
||||
),
|
||||
root,
|
||||
"build",
|
||||
)
|
||||
)
|
||||
test = ValidationStep(
|
||||
CommandSpec(_test_argv(root, config.msbuild.test_command), root, "test")
|
||||
)
|
||||
return ValidationPlan(ProjectKind.MSBUILD, (build, test))
|
||||
@@ -0,0 +1,146 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .models import CMakeConfig, HarnessConfig, MSBuildConfig, TDDConfig
|
||||
|
||||
DEFAULT_TEST_PATTERNS = (
|
||||
"{stem}_test.cpp",
|
||||
"{stem}_tests.cpp",
|
||||
"test_{stem}.cpp",
|
||||
"{stem}.test.cpp",
|
||||
)
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _reject_unknown(data: dict[str, Any], allowed: set[str], label: str) -> None:
|
||||
unknown = sorted(set(data) - allowed)
|
||||
if unknown:
|
||||
raise ConfigError(f"{label} contains unexpected field: {unknown[0]}")
|
||||
|
||||
|
||||
def _repo_path(root: Path, raw: str, label: str) -> Path:
|
||||
candidate = Path(raw)
|
||||
if candidate.is_absolute():
|
||||
raise ConfigError(f"{label} must be repository-relative")
|
||||
resolved = (root / candidate).resolve()
|
||||
try:
|
||||
resolved.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ConfigError(f"{label} resolves outside the repository") from exc
|
||||
return resolved
|
||||
|
||||
|
||||
def _mapping(value: Any, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise ConfigError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _nonempty_string(value: Any, label: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ConfigError(f"{label} must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
def _string_array(value: Any, label: str) -> tuple[str, ...]:
|
||||
if not isinstance(value, list):
|
||||
raise ConfigError(f"{label} must be an array")
|
||||
if not value or any(not isinstance(item, str) or not item for item in value):
|
||||
raise ConfigError(f"{label} must be a non-empty string array")
|
||||
return tuple(value)
|
||||
|
||||
|
||||
def load_config(root: Path) -> HarnessConfig:
|
||||
root = root.resolve()
|
||||
config_path = root / ".harness" / "config.json"
|
||||
if config_path.is_file():
|
||||
try:
|
||||
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(
|
||||
f"{config_path}: configuration must be valid UTF-8: {exc}"
|
||||
) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ConfigError(f"{config_path}: {exc}") from exc
|
||||
else:
|
||||
data = {"version": 1}
|
||||
|
||||
data = _mapping(data, "config")
|
||||
_reject_unknown(data, {"version", "projectType", "cmake", "msbuild", "tdd"}, "config")
|
||||
|
||||
if data.get("version") != 1 or isinstance(data.get("version"), bool):
|
||||
raise ConfigError("version must be 1")
|
||||
project_type = data.get("projectType", "auto")
|
||||
if project_type not in {"auto", "cmake", "msbuild"}:
|
||||
raise ConfigError("projectType must be auto, cmake, or msbuild")
|
||||
|
||||
cmake_data = _mapping(data.get("cmake", {}), "cmake")
|
||||
_reject_unknown(
|
||||
cmake_data,
|
||||
{"sourceDir", "binaryDir", "configurePreset", "buildPreset", "testPreset"},
|
||||
"cmake",
|
||||
)
|
||||
source_dir = _repo_path(
|
||||
root, _nonempty_string(cmake_data.get("sourceDir", "."), "cmake.sourceDir"), "cmake.sourceDir"
|
||||
)
|
||||
binary_dir = None
|
||||
if "binaryDir" in cmake_data:
|
||||
binary_dir = _repo_path(
|
||||
root, _nonempty_string(cmake_data["binaryDir"], "cmake.binaryDir"), "cmake.binaryDir"
|
||||
)
|
||||
preset_keys = ("configurePreset", "buildPreset", "testPreset", "binaryDir")
|
||||
if any(key in cmake_data for key in preset_keys) and not all(key in cmake_data for key in preset_keys):
|
||||
raise ConfigError("cmake configurePreset, buildPreset, testPreset, and binaryDir must be specified together")
|
||||
configure_preset = (
|
||||
_nonempty_string(cmake_data["configurePreset"], "cmake.configurePreset")
|
||||
if "configurePreset" in cmake_data
|
||||
else None
|
||||
)
|
||||
build_preset = (
|
||||
_nonempty_string(cmake_data["buildPreset"], "cmake.buildPreset")
|
||||
if "buildPreset" in cmake_data
|
||||
else None
|
||||
)
|
||||
test_preset = (
|
||||
_nonempty_string(cmake_data["testPreset"], "cmake.testPreset")
|
||||
if "testPreset" in cmake_data
|
||||
else None
|
||||
)
|
||||
|
||||
msbuild_data = _mapping(data.get("msbuild", {}), "msbuild")
|
||||
_reject_unknown(msbuild_data, {"solution", "configuration", "platform", "testCommand"}, "msbuild")
|
||||
solution = None
|
||||
if "solution" in msbuild_data:
|
||||
solution = _repo_path(
|
||||
root, _nonempty_string(msbuild_data["solution"], "msbuild.solution"), "msbuild.solution"
|
||||
)
|
||||
configuration = _nonempty_string(msbuild_data.get("configuration", "Debug"), "msbuild.configuration")
|
||||
platform = _nonempty_string(msbuild_data.get("platform", "x64"), "msbuild.platform")
|
||||
test_command = (
|
||||
_string_array(msbuild_data["testCommand"], "msbuild.testCommand")
|
||||
if "testCommand" in msbuild_data
|
||||
else None
|
||||
)
|
||||
|
||||
tdd_data = _mapping(data.get("tdd", {}), "tdd")
|
||||
_reject_unknown(tdd_data, {"testRoots", "testPatterns", "exclude"}, "tdd")
|
||||
raw_test_roots = _string_array(tdd_data["testRoots"], "tdd.testRoots") if "testRoots" in tdd_data else ("tests", "test")
|
||||
test_roots = tuple(_repo_path(root, item, "tdd.testRoots") for item in raw_test_roots)
|
||||
test_patterns = _string_array(tdd_data["testPatterns"], "tdd.testPatterns") if "testPatterns" in tdd_data else DEFAULT_TEST_PATTERNS
|
||||
if any("{stem}" not in pattern for pattern in test_patterns):
|
||||
raise ConfigError("every tdd.testPatterns entry must contain {stem}")
|
||||
exclude = _string_array(tdd_data["exclude"], "tdd.exclude") if "exclude" in tdd_data else ()
|
||||
for item in exclude:
|
||||
_repo_path(root, item, "tdd.exclude")
|
||||
|
||||
return HarnessConfig(
|
||||
version=1,
|
||||
project_type=project_type,
|
||||
cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset),
|
||||
msbuild=MSBuildConfig(solution, configuration, platform, test_command),
|
||||
tdd=TDDConfig(test_roots, test_patterns, exclude),
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
from pathlib import Path
|
||||
|
||||
from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection
|
||||
from .tdd_policy import find_cpp_files
|
||||
|
||||
|
||||
class DiscoveryError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _cmake_entry(source: Path) -> Path | None:
|
||||
for name in ("CMakePresets.json", "CMakeUserPresets.json", "CMakeLists.txt"):
|
||||
candidate = source / name
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _single_msbuild(root: Path, explicit: Path | None) -> Path:
|
||||
if explicit is not None:
|
||||
if explicit.is_file() and explicit.suffix.lower() in {".sln", ".vcxproj"}:
|
||||
return explicit
|
||||
raise DiscoveryError(f"configured MSBuild project is invalid: {explicit}")
|
||||
solutions = sorted(root.glob("*.sln"))
|
||||
if len(solutions) > 1:
|
||||
raise DiscoveryError("multiple .sln files found; set msbuild.solution")
|
||||
if solutions:
|
||||
return solutions[0]
|
||||
projects = sorted(root.glob("*.vcxproj"))
|
||||
if len(projects) > 1:
|
||||
raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution")
|
||||
if projects:
|
||||
return projects[0]
|
||||
raise DiscoveryError("no root .sln or .vcxproj was found")
|
||||
|
||||
|
||||
def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult:
|
||||
root = root.resolve()
|
||||
if config.project_type == "cmake":
|
||||
entry = _cmake_entry(config.cmake.source_dir)
|
||||
if entry is None:
|
||||
raise DiscoveryError(
|
||||
f"no CMake project found in {config.cmake.source_dir}"
|
||||
)
|
||||
return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry))
|
||||
|
||||
if config.project_type == "msbuild":
|
||||
project = _single_msbuild(root, config.msbuild.solution)
|
||||
return DiscoveryResult(ProjectSelection(ProjectKind.MSBUILD, project))
|
||||
|
||||
entry = _cmake_entry(root)
|
||||
if entry is not None:
|
||||
return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry))
|
||||
|
||||
solutions = sorted(root.glob("*.sln"))
|
||||
if len(solutions) > 1:
|
||||
raise DiscoveryError("multiple .sln files found; set msbuild.solution")
|
||||
if solutions:
|
||||
return DiscoveryResult(
|
||||
ProjectSelection(ProjectKind.MSBUILD, solutions[0])
|
||||
)
|
||||
|
||||
projects = sorted(root.glob("*.vcxproj"))
|
||||
if len(projects) > 1:
|
||||
raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution")
|
||||
if projects:
|
||||
return DiscoveryResult(
|
||||
ProjectSelection(ProjectKind.MSBUILD, projects[0])
|
||||
)
|
||||
|
||||
cpp_files = find_cpp_files(root)
|
||||
if cpp_files:
|
||||
raise DiscoveryError(
|
||||
"C/C++ files exist but no CMakeLists.txt, preset, .sln, or .vcxproj "
|
||||
"was found; configure projectType and project path"
|
||||
)
|
||||
return DiscoveryResult(None, ())
|
||||
@@ -0,0 +1,100 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ProjectKind(Enum):
|
||||
CMAKE = "cmake"
|
||||
MSBUILD = "msbuild"
|
||||
|
||||
|
||||
class ResultCheckKind(Enum):
|
||||
CMAKE_COMPILER_IS_MSVC = "cmake_compiler_is_msvc"
|
||||
CTEST_HAS_TESTS = "ctest_has_tests"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CMakeConfig:
|
||||
source_dir: Path
|
||||
binary_dir: Path | None = None
|
||||
configure_preset: str | None = None
|
||||
build_preset: str | None = None
|
||||
test_preset: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MSBuildConfig:
|
||||
solution: Path | None = None
|
||||
configuration: str = "Debug"
|
||||
platform: str = "x64"
|
||||
test_command: tuple[str, ...] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TDDConfig:
|
||||
test_roots: tuple[Path, ...]
|
||||
test_patterns: tuple[str, ...]
|
||||
exclude: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HarnessConfig:
|
||||
version: int
|
||||
project_type: str
|
||||
cmake: CMakeConfig
|
||||
msbuild: MSBuildConfig
|
||||
tdd: TDDConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProjectSelection:
|
||||
kind: ProjectKind
|
||||
project_file: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiscoveryResult:
|
||||
selection: ProjectSelection | None
|
||||
cpp_files: tuple[Path, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Toolchain:
|
||||
installation: Path
|
||||
msbuild: Path
|
||||
cmake: Path | None = None
|
||||
ctest: Path | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandSpec:
|
||||
argv: tuple[str, ...]
|
||||
cwd: Path
|
||||
stage: str
|
||||
timeout_seconds: int = 1800
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResultCheck:
|
||||
kind: ResultCheckKind
|
||||
path: Path | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidationStep:
|
||||
command: CommandSpec
|
||||
checks: tuple[ResultCheck, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidationPlan:
|
||||
project_kind: ProjectKind
|
||||
steps: tuple[ValidationStep, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandResult:
|
||||
command: CommandSpec
|
||||
returncode: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
import locale
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from .models import CommandResult, ResultCheckKind
|
||||
|
||||
DIAGNOSTIC_LIMIT = 8000
|
||||
|
||||
|
||||
class ValidationFailure(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _decode(value):
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
for encoding in ("utf-8", locale.getpreferredencoding(False)):
|
||||
try:
|
||||
return value.decode(encoding)
|
||||
except (UnicodeDecodeError, LookupError):
|
||||
continue
|
||||
return value.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def _check_result(check, result):
|
||||
if check.kind is ResultCheckKind.CMAKE_COMPILER_IS_MSVC:
|
||||
if check.path is None:
|
||||
raise ValidationFailure("MSVC check is missing binaryDir")
|
||||
files = sorted(check.path.glob("CMakeFiles/*/CMakeCXXCompiler.cmake"))
|
||||
if not files:
|
||||
raise ValidationFailure("CMake did not generate compiler metadata")
|
||||
text = files[-1].read_text(encoding="utf-8", errors="replace")
|
||||
if not re.search(
|
||||
r'set\s*\(\s*CMAKE_CXX_COMPILER_ID\s+"MSVC"\s*\)', text
|
||||
):
|
||||
raise ValidationFailure("CMake selected a compiler other than MSVC")
|
||||
elif check.kind is ResultCheckKind.CTEST_HAS_TESTS:
|
||||
try:
|
||||
payload = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValidationFailure("CTest discovery did not return JSON") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise ValidationFailure("CTest discovery JSON must be an object")
|
||||
if not isinstance(payload.get("tests"), list) or not payload["tests"]:
|
||||
raise ValidationFailure("CTest discovered no tests")
|
||||
|
||||
|
||||
def _diagnostic(stdout, stderr):
|
||||
diagnostic = (stdout + "\n" + stderr).rstrip()[-DIAGNOSTIC_LIMIT:]
|
||||
return diagnostic.replace("\r\n", " | ").replace("\n", " | ")
|
||||
|
||||
|
||||
def _command_context(argv, cwd):
|
||||
return f"argv={list(argv)!r}; cwd={str(cwd)!r}"
|
||||
|
||||
|
||||
def execute_plan(
|
||||
plan,
|
||||
root,
|
||||
*,
|
||||
env=None,
|
||||
total_timeout_seconds=1800,
|
||||
deadline=None,
|
||||
run=subprocess.run,
|
||||
clock=time.monotonic,
|
||||
):
|
||||
root = root.resolve()
|
||||
if deadline is None:
|
||||
deadline = clock() + total_timeout_seconds
|
||||
results = []
|
||||
|
||||
for step in plan.steps:
|
||||
cwd = step.command.cwd.resolve()
|
||||
context = _command_context(step.command.argv, cwd)
|
||||
try:
|
||||
cwd.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValidationFailure(
|
||||
f"{step.command.stage} working directory is outside the repository; "
|
||||
f"{context}"
|
||||
) from exc
|
||||
|
||||
remaining = deadline - clock()
|
||||
if remaining <= 0:
|
||||
raise ValidationFailure(
|
||||
f"{step.command.stage} timed out before it could start; {context}"
|
||||
)
|
||||
|
||||
timeout = min(step.command.timeout_seconds, remaining)
|
||||
try:
|
||||
completed = run(
|
||||
list(step.command.argv),
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
shell=False,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
stdout = _decode(exc.output)
|
||||
stderr = _decode(exc.stderr)
|
||||
diagnostic = _diagnostic(stdout, stderr)
|
||||
message = (
|
||||
f"{step.command.stage} timed out after {timeout} seconds; {context}"
|
||||
)
|
||||
if diagnostic:
|
||||
message = f"{message}; output tail: {diagnostic}"
|
||||
raise ValidationFailure(message) from exc
|
||||
|
||||
stdout = _decode(completed.stdout)
|
||||
stderr = _decode(completed.stderr)
|
||||
result = CommandResult(step.command, completed.returncode, stdout, stderr)
|
||||
if completed.returncode != 0:
|
||||
diagnostic = _diagnostic(stdout, stderr)
|
||||
message = (
|
||||
f"{step.command.stage} failed with exit code {completed.returncode}; "
|
||||
f"{context}"
|
||||
)
|
||||
if diagnostic:
|
||||
message = f"{message}; output tail: {diagnostic}"
|
||||
raise ValidationFailure(message)
|
||||
|
||||
try:
|
||||
for check in step.checks:
|
||||
_check_result(check, result)
|
||||
except ValidationFailure as exc:
|
||||
diagnostic = _diagnostic(stdout, stderr)
|
||||
message = (
|
||||
f"{step.command.stage} result check failed: {exc}; {context}"
|
||||
)
|
||||
if diagnostic:
|
||||
message = f"{message}; output tail: {diagnostic}"
|
||||
raise ValidationFailure(message) from exc
|
||||
results.append(result)
|
||||
|
||||
return tuple(results)
|
||||
@@ -0,0 +1,88 @@
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
|
||||
CPP_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx"})
|
||||
DEFAULT_EXCLUDES = (
|
||||
".harness/build/**",
|
||||
"build/**",
|
||||
"out/**",
|
||||
"cmake-build-*/**",
|
||||
"third_party/**",
|
||||
"external/**",
|
||||
"vendor/**",
|
||||
"generated/**",
|
||||
)
|
||||
|
||||
|
||||
def is_excluded_path(path: Path, root: Path, extra: tuple[str, ...] = ()) -> bool:
|
||||
try:
|
||||
relative = path.resolve().relative_to(root.resolve()).as_posix()
|
||||
except ValueError:
|
||||
return False
|
||||
return any(fnmatch(relative, pattern) for pattern in DEFAULT_EXCLUDES + extra)
|
||||
|
||||
|
||||
def find_cpp_files(
|
||||
root: Path,
|
||||
extra_excludes: tuple[str, ...] = (),
|
||||
) -> tuple[Path, ...]:
|
||||
found = (
|
||||
path
|
||||
for path in root.resolve().rglob("*")
|
||||
if path.is_file()
|
||||
and path.suffix.lower() in CPP_SUFFIXES
|
||||
and not is_excluded_path(path, root, extra_excludes)
|
||||
)
|
||||
return tuple(sorted(found))
|
||||
|
||||
|
||||
def is_test_file(path: Path, root: Path, config) -> bool:
|
||||
resolved = path.resolve()
|
||||
if any(root == resolved or root in resolved.parents for root in config.test_roots):
|
||||
return True
|
||||
relative = resolved.relative_to(root.resolve())
|
||||
if {"test", "tests"} & {part.lower() for part in relative.parts}:
|
||||
return True
|
||||
stem = resolved.stem.lower()
|
||||
return stem.startswith("test_") or stem.endswith(("_test", "_tests", ".test"))
|
||||
|
||||
|
||||
def matching_test_exists(path: Path, config) -> bool:
|
||||
names = tuple(pattern.format(stem=path.stem) for pattern in config.test_patterns)
|
||||
roots = (
|
||||
*config.test_roots,
|
||||
path.parent / "tests",
|
||||
path.parent / "test",
|
||||
)
|
||||
for root in roots:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for name in names:
|
||||
if any(candidate.is_file() for candidate in root.rglob(name)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def evaluate_paths(paths, root: Path, config) -> str | None:
|
||||
root = root.resolve()
|
||||
for raw in paths:
|
||||
path = raw.resolve()
|
||||
try:
|
||||
path.relative_to(root)
|
||||
except ValueError:
|
||||
return f"TDD GUARD: '{path}' is outside the repository"
|
||||
if path.suffix.lower() not in CPP_SUFFIXES:
|
||||
continue
|
||||
if (
|
||||
path.name.lower() == "main.cpp"
|
||||
or is_excluded_path(path, root, config.exclude)
|
||||
or is_test_file(path, root, config)
|
||||
):
|
||||
continue
|
||||
if not matching_test_exists(path, config):
|
||||
expected = config.test_patterns[0].format(stem=path.stem)
|
||||
return (
|
||||
f"TDD GUARD: '{path.name}' requires an existing test such as "
|
||||
f"'{expected}'. Add the test in a configured test root first."
|
||||
)
|
||||
return None
|
||||
@@ -0,0 +1,169 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from .models import ProjectKind, Toolchain
|
||||
|
||||
VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
|
||||
VS_CMAKE_RELATIVE = Path(
|
||||
"Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin"
|
||||
)
|
||||
|
||||
|
||||
class ToolchainError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _decode(value):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return (value or b"").decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def _first_path(result, label, argv, cwd):
|
||||
text = _decode(result.stdout).strip()
|
||||
if result.returncode != 0:
|
||||
diagnostic = (
|
||||
_decode(result.stdout) + "\n" + _decode(result.stderr)
|
||||
).rstrip()[-8000:]
|
||||
diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
|
||||
message = (
|
||||
f"{label} probe failed with exit code {result.returncode}; "
|
||||
f"argv={argv!r}; cwd={str(cwd)!r}"
|
||||
)
|
||||
if diagnostic:
|
||||
message = f"{message}; output tail: {diagnostic}"
|
||||
raise ToolchainError(
|
||||
f"{message}; install Visual Studio Desktop development with C++"
|
||||
)
|
||||
if not text:
|
||||
raise ToolchainError(
|
||||
f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; "
|
||||
"install Visual Studio Desktop development with C++"
|
||||
)
|
||||
path = Path(text.splitlines()[0]).resolve()
|
||||
if not path.exists():
|
||||
diagnostic = (
|
||||
_decode(result.stdout) + "\n" + _decode(result.stderr)
|
||||
).rstrip()[-8000:]
|
||||
diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
|
||||
message = (
|
||||
f"{label} probe returned nonexistent path with exit code 0; "
|
||||
f"argv={argv!r}; cwd={str(cwd)!r}"
|
||||
)
|
||||
if diagnostic:
|
||||
message = f"{message}; output tail: {diagnostic}"
|
||||
raise ToolchainError(
|
||||
f"{message}; install or repair Visual Studio Desktop development with C++"
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def _require_path(path, label):
|
||||
if path is None or not path.exists():
|
||||
raise ToolchainError(f"{label} not found")
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _remaining(deadline, clock, label):
|
||||
if deadline is None:
|
||||
return None
|
||||
remaining = deadline - clock()
|
||||
if remaining <= 0:
|
||||
raise ToolchainError(f"{label} probe timed out before it could start")
|
||||
return remaining
|
||||
|
||||
|
||||
def _probe(run, argv, label, *, deadline, clock):
|
||||
cwd = Path.cwd().resolve()
|
||||
kwargs = {
|
||||
"cwd": cwd,
|
||||
"capture_output": True,
|
||||
"check": False,
|
||||
"shell": False,
|
||||
}
|
||||
remaining = _remaining(deadline, clock, label)
|
||||
if remaining is not None:
|
||||
kwargs["timeout"] = remaining
|
||||
try:
|
||||
return run(argv, **kwargs)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ToolchainError(
|
||||
f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; "
|
||||
"install or repair Visual Studio Desktop development with C++"
|
||||
) from exc
|
||||
|
||||
|
||||
def discover_toolchain(
|
||||
kind: ProjectKind,
|
||||
*,
|
||||
env=None,
|
||||
which=shutil.which,
|
||||
run=subprocess.run,
|
||||
deadline=None,
|
||||
clock=time.monotonic,
|
||||
) -> Toolchain:
|
||||
environment = os.environ if env is None else env
|
||||
vswhere_path = which("vswhere.exe")
|
||||
if vswhere_path is None:
|
||||
program_files = environment.get("ProgramFiles(x86)")
|
||||
if program_files:
|
||||
candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe"
|
||||
vswhere_path = str(candidate) if candidate.exists() else None
|
||||
vswhere = _require_path(
|
||||
Path(vswhere_path) if vswhere_path else None,
|
||||
"vswhere.exe; install Visual Studio Desktop development with C++",
|
||||
)
|
||||
|
||||
install_argv = [
|
||||
str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
|
||||
"-property", "installationPath",
|
||||
]
|
||||
install_result = _probe(
|
||||
run,
|
||||
install_argv,
|
||||
"Visual Studio installation",
|
||||
deadline=deadline,
|
||||
clock=clock,
|
||||
)
|
||||
probe_cwd = Path.cwd().resolve()
|
||||
installation = _require_path(
|
||||
_first_path(
|
||||
install_result,
|
||||
"Visual Studio installation",
|
||||
install_argv,
|
||||
probe_cwd,
|
||||
),
|
||||
"Visual Studio installation",
|
||||
)
|
||||
msbuild_argv = [
|
||||
str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
|
||||
"-find", r"MSBuild\**\Bin\MSBuild.exe",
|
||||
]
|
||||
msbuild_result = _probe(
|
||||
run,
|
||||
msbuild_argv,
|
||||
"MSBuild",
|
||||
deadline=deadline,
|
||||
clock=clock,
|
||||
)
|
||||
msbuild = _require_path(
|
||||
_first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd),
|
||||
"MSBuild.exe",
|
||||
)
|
||||
|
||||
if kind is not ProjectKind.CMAKE:
|
||||
return Toolchain(installation=installation, msbuild=msbuild)
|
||||
|
||||
bundled_cmake_dir = installation / VS_CMAKE_RELATIVE
|
||||
cmake = _require_path(
|
||||
Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe",
|
||||
"cmake.exe",
|
||||
)
|
||||
ctest = _require_path(
|
||||
Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe",
|
||||
"ctest.exe",
|
||||
)
|
||||
return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest)
|
||||
Reference in New Issue
Block a user