diff --git a/.agents/skills/harness/SKILL.md b/.agents/skills/harness/SKILL.md new file mode 100644 index 0000000..49519ee --- /dev/null +++ b/.agents/skills/harness/SKILL.md @@ -0,0 +1,172 @@ +--- +name: harness +description: Use when planning agentic implementation phases, creating phases/index.json and self-contained step files, or running the Harness step executor. +--- + +# Harness Workflow + +이 프로젝트는 Harness 프레임워크를 사용한다. 아래 워크플로에 따라 작업한다. + +## A. 탐색 + +`AGENTS.md`와 `docs/` 하위 문서(PRD, ARCHITECTURE, ADR 등)를 읽고 프로젝트의 기획, +아키텍처, 설계 의도를 파악한다. 병렬 탐색이 실제로 유용하고 현재 세션에서 허용될 +때만 Codex subagent를 선택적으로 사용한다. + +## B. 논의 + +구현을 위해 구체화하거나 기술적으로 결정해야 할 사항이 있으면 사용자에게 한 번에 +하나씩 제시하고 논의한다. + +## C. Step 설계 + +사용자가 구현 계획 작성을 지시하면 여러 step으로 나뉜 초안을 작성해 피드백을 +요청한다. + +설계 원칙: + +1. **Scope 최소화** — 하나의 step에서 하나의 레이어 또는 모듈만 다룬다. 여러 + 모듈을 동시에 수정해야 하면 step을 쪼갠다. +2. **자기완결성** — 각 step 파일은 독립된 Codex 실행에서 사용된다. 외부 대화 + 참조를 금지하고 필요한 정보를 모두 파일 안에 적는다. +3. **사전 준비 강제** — 관련 문서와 이전 step에서 생성하거나 수정한 파일 경로를 + 명시한다. +4. **시그니처 수준 지시** — 함수와 클래스의 인터페이스를 제시하고 내부 구현은 + Codex 재량에 맡긴다. 멱등성, 보안, 데이터 무결성 같은 핵심 규칙은 명시한다. +5. **AC는 실행 가능한 command** — 추상적 조건 대신 실제 빌드와 테스트 command를 + 포함한다. +6. **주의사항은 구체적으로** — "X를 하지 마라. 이유: Y" 형식으로 적는다. +7. **네이밍** — step name은 핵심 작업을 표현하는 kebab-case slug로 정한다. + +## D. 파일 생성 + +사용자가 초안을 승인한 후에만 다음 파일을 생성한다. + +### D-1. `phases/index.json` + +여러 task를 관리하는 top-level 인덱스다. 이미 존재하면 `phases` 배열에 새 항목을 +추가한다. + +```json +{ + "phases": [ + { + "dir": "0-mvp", + "status": "pending" + } + ] +} +``` + +- `dir`: task 디렉터리명 +- `status`: `pending` | `completed` | `error` | `blocked` +- timestamp는 executor가 상태를 바꿀 때 기록하므로 생성 시 넣지 않는다. + +### D-2. `phases/{task-name}/index.json` + +```json +{ + "project": "<프로젝트명>", + "phase": "", + "steps": [ + { "step": 0, "name": "project-setup", "status": "pending" }, + { "step": 1, "name": "core-types", "status": "pending" }, + { "step": 2, "name": "api-layer", "status": "pending" } + ] +} +``` + +필드 규칙: + +- `project`: `AGENTS.md`에 정의된 프로젝트명 +- `phase`: task 이름이며 디렉터리명과 일치 +- `steps[].step`: 0부터 시작하는 순번 +- `steps[].name`: kebab-case slug +- `steps[].status`: 초기값 `pending` + +상태와 기록 주체: + +| 전이 | 기록 필드 | 기록 주체 | +|------|-----------|-----------| +| `completed` | `summary`, `completed_at` | Codex가 summary, executor가 timestamp | +| `error` | `error_message`, `failed_at` | Codex가 message, executor가 timestamp | +| `blocked` | `blocked_reason`, `blocked_at` | Codex가 reason, executor가 timestamp | + +`summary`에는 다음 step에 유용한 생성 파일과 핵심 결정을 한 줄로 적는다. +task `created_at`과 step `started_at`은 executor가 기록하므로 생성 시 넣지 않는다. + +### D-3. `phases/{task-name}/step{N}.md` + +````markdown +# Step {N}: {이름} + +## 읽어야 할 파일 + +먼저 아래 파일을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라: + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- 이전 step에서 생성하거나 수정한 파일 경로 + +이전 step의 코드를 꼼꼼히 읽고 설계 의도를 이해한 뒤 작업하라. + +## 작업 + +구체적인 구현 지시를 파일 경로, 클래스와 함수 시그니처, 로직 설명과 함께 적는다. +구현체는 Codex에 맡기되 설계 의도에서 벗어나면 안 되는 핵심 규칙은 명시한다. + +## Acceptance Criteria + +프로젝트 형식에 맞는 명령을 사용한다. `.harness/config.json`이 있으면 해당 preset, +solution, configuration, platform, test command를 우선한다. + +```powershell +# CMake +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --output-on-failure + +# 직접 MSBuild +MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64 +.\build\tests\Debug\MyProjectTests.exe +``` + +## 검증 절차 + +1. Acceptance Criteria command를 실행한다. +2. ARCHITECTURE 디렉터리 구조를 따르는지 확인한다. +3. ADR 기술 스택과 `AGENTS.md` CRITICAL 규칙을 확인한다. +4. 결과에 따라 task index의 해당 step을 갱신한다. + - 성공: `status`를 `completed`로 바꾸고 한 줄 `summary` 기록 + - 수정 3회 후 실패: `status`를 `error`로 바꾸고 `error_message` 기록 + - 사용자 개입 필요: `status`를 `blocked`로 바꾸고 `blocked_reason` 기록 후 중단 + +## 금지사항 + +- 이 step의 범위 밖 기능을 추가하지 마라. 이유: step의 독립성을 깨뜨린다. +- 기존 테스트를 깨뜨리지 마라. 이유: 이전 동작을 회귀시킨다. +```` + +## E. 실행 + +```bash +python scripts/execute.py {task-name} +python scripts/execute.py {task-name} --push +``` + +환경에서 Python 3 실행 명령이 `python3`이면 그 명령을 대신 사용한다. + +executor가 처리하는 작업: + +- `feat-{task-name}` 브랜치 생성 또는 checkout +- `AGENTS.md`와 `docs/*.md` guardrail 주입 +- 완료 step의 summary를 다음 prompt에 누적 +- 실패 시 최대 3회 재시도하며 이전 오류를 prompt에 전달 +- 코드 변경과 metadata를 분리해 commit +- `started_at`, `completed_at`, `failed_at`, `blocked_at` 기록 + +에러 복구: + +- `error`: 해당 status를 `pending`으로 바꾸고 `error_message`를 삭제한 뒤 재실행 +- `blocked`: 원인을 해결하고 status를 `pending`으로 바꾸고 `blocked_reason`을 삭제한 + 뒤 재실행 diff --git a/.agents/skills/review/SKILL.md b/.agents/skills/review/SKILL.md new file mode 100644 index 0000000..9f695da --- /dev/null +++ b/.agents/skills/review/SKILL.md @@ -0,0 +1,41 @@ +--- +name: review +description: Use when reviewing repository changes against AGENTS.md, architecture decisions, tests, and build requirements. +--- + +# Repository Review + +먼저 다음 문서를 읽는다. + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` + +사용자가 범위를 지정하지 않으면 현재 작업 트리의 변경을 리뷰한다. 관련 diff를 +확인하고 가능한 빌드와 테스트 command를 실제로 실행한다. + +## 체크리스트 + +1. MSVC toolset와 C++ 표준이 AGENTS.md/ADR과 일치하는가? +2. CMake 또는 MSBuild Debug/x64 빌드가 통과하는가? +3. CTest 또는 `.harness/config.json`의 명시적 test command가 통과하는가? +4. 새 C/C++ 소스와 헤더에 대응 테스트가 있는가? +5. CRITICAL 아키텍처 규칙과 public header 경계를 지키는가? + +## 출력 형식 + +실제 결함을 심각도순으로 먼저 제시한다. 각 finding에 파일과 줄 번호, 영향, +재현 또는 근거, 구체적인 수정 방안을 포함한다. + +그 뒤 다음 표를 제공한다. + +| 항목 | 결과 | 비고 | +|------|------|------| +| 아키텍처 준수 | ✅/❌/미검증 | 상세 | +| 기술 스택 준수 | ✅/❌/미검증 | 상세 | +| 테스트 존재 | ✅/❌/미검증 | 상세 | +| CRITICAL 규칙 | ✅/❌/미검증 | 상세 | +| 빌드 가능 | ✅/❌/미검증 | 상세 | + +실행할 수 없는 검사는 성공으로 추정하지 말고 `미검증`과 이유를 적는다. finding이 +없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다. diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..598631c --- /dev/null +++ b/.codex/hooks.json @@ -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" + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 88dbff1..413c791 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.vs/ +__pycache__/ +.pytest_cache/ +*.py[cod] +.harness/build/ +.worktrees/ diff --git a/.harness/config.example.json b/.harness/config.example.json new file mode 100644 index 0000000..70463a7 --- /dev/null +++ b/.harness/config.example.json @@ -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/**" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..44d9209 --- /dev/null +++ b/AGENTS.md @@ -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 +``` + +사용자가 명시적으로 원격 push를 요청한 경우에만 다음을 사용한다. + +```powershell +python scripts/execute.py --push +``` diff --git a/docs/ADR.md b/docs/ADR.md new file mode 100644 index 0000000..80217e1 --- /dev/null +++ b/docs/ADR.md @@ -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`에서 관리한다. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..98524f7 --- /dev/null +++ b/docs/ARCHITECTURE.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.inp + _displacements.csv + _reactions.csv + _internalforces.csv + _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//frames/ +│ ├── 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/`: 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/` 브랜치 및 명시적 clean-worktree 정책은 FESA 아키텍처의 +요구사항으로 간주하지 않는다. diff --git a/docs/HARNESS.md b/docs/HARNESS.md new file mode 100644 index 0000000..feff168 --- /dev/null +++ b/docs/HARNESS.md @@ -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 +python scripts/execute.py --push +``` + +## Harness Python 검증 + +이 저장소의 테스트와 최종 acceptance 검증은 pytest를 시스템 Python에 설치하지 않고 +다음 명령으로 실행한다. + +```powershell +uv run --with pytest python -m pytest -v -rs +``` + +## CMake preset 설정 + +`projectType`을 `cmake`로 지정하거나 자동 감지를 사용한다. `cmake.sourceDir`, +`binaryDir`, `configurePreset`, `buildPreset`, `testPreset`은 preset을 사용할 때 함께 +지정해야 한다. 빌드 산출물은 저장소의 `.harness/build/`처럼 격리된 경로에 둔다. + +```json +{ + "version": 1, + "projectType": "cmake", + "cmake": { + "sourceDir": ".", + "binaryDir": "out/build/windows-debug", + "configurePreset": "windows-debug", + "buildPreset": "windows-debug", + "testPreset": "windows-debug" + } +} +``` + +```powershell +cmake --preset windows-debug +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +``` + +Preset을 쓰지 않는 경우에는 같은 격리된 build directory를 명시한다. + +```powershell +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +ctest --test-dir .harness/build -C Debug --output-on-failure +``` + +## 직접 MSBuild 설정 + +`projectType`을 `msbuild`로 설정하면 `msbuild.solution`, `configuration`, `platform`을 +지정한다. 직접 MSBuild 프로젝트에서는 `msbuild.testCommand`가 필수이며, 테스트 실행 +파일과 인수를 JSON 배열로 적는다. + +```json +{ + "version": 1, + "projectType": "msbuild", + "msbuild": { + "solution": "MyProject.sln", + "configuration": "Debug", + "platform": "x64", + "testCommand": ["build/tests/Debug/MyProjectTests.exe"] + } +} +``` + +```powershell +MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64 +.\build\tests\Debug\MyProjectTests.exe +``` + +## TDD 확장 + +`tdd.testRoots`와 `tdd.testPatterns`로 테스트 위치와 이름을 확장한다. 패턴마다 +`{stem}`이 필요하다. `main`, 테스트, 외부 의존성, 생성 파일, build directory 같은 +기본 제외 항목은 Harness가 관리하며, `tdd.exclude`의 사용자 제외 항목은 이를 +대체하지 않고 추가한다. + +```json +{ + "version": 1, + "tdd": { + "testRoots": ["tests", "integration-tests"], + "testPatterns": ["{stem}_test.cpp", "test_{stem}.cpp"], + "exclude": ["legacy/generated/**"] + } +} +``` + +## 실패 복구 + +- Visual Studio C++ workload가 없으면 Installer에서 Desktop development with C++를 설치한 뒤 다시 실행한다. +- solution 또는 project가 여러 개라서 모호하면 `projectType`과 `msbuild.solution`을 명시한다. +- MSVC가 아닌 컴파일러가 감지되면 MSVC Developer Command Prompt에서 실행하거나 toolchain을 MSVC로 전환한다. +- CTest가 0개 테스트를 보고하면 `enable_testing()`과 테스트 등록을 확인한다. +- 직접 MSBuild 구성에 test command가 없으면 `msbuild.testCommand` 배열을 추가한다. +- timeout 또는 명령 실패 시 Stop 응답의 stage, 안전한 argv 배열, 작업 디렉터리, + 종료 코드와 출력 tail을 확인하고 해당 명령을 단독으로 다시 실행한다. Harness는 + 별도의 로그 파일을 만들지 않는다. diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..8ff1902 --- /dev/null +++ b/docs/PRD.md @@ -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//`에 보관하며 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, 입력 부분집합, 정식화 및 검증 보고서 제공 diff --git a/docs/superpowers/plans/2026-07-29-fesa-phase-1.md b/docs/superpowers/plans/2026-07-29-fesa-phase-1.md new file mode 100644 index 0000000..f4934b5 --- /dev/null +++ b/docs/superpowers/plans/2026-07-29-fesa-phase-1.md @@ -0,0 +1,1366 @@ +# FESA Phase 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build and internally qualify a C++20/MSVC linear-static finite-element solver that reads the agreed Abaqus `.inp` subset, solves 2-node 3D Timoshenko Beam models, and writes self-contained HDF5 results. + +**Architecture:** Implement one end-to-end vertical slice first, then complete the input, numerical, parallel, result, and reference-verification contracts behind the module boundaries in `docs/ARCHITECTURE.md`. Keep the semantic model independent of Abaqus syntax and isolate oneMKL, oneTBB, and HDF5 behind adapters. + +**Tech Stack:** C++20, Visual Studio 2022 MSVC v143 x64, CMake/CMake Presets/CTest, GoogleTest/GoogleMock, Intel oneAPI MKL PARDISO, Intel oneAPI TBB, HDF5 C API, Python 3 Harness. + +## Global Constraints + +- Only the Phase 1 scope in `docs/PRD.md` may be implemented. +- Public headers live under `include/fesa/`; implementations live under `src/fesa/`; tests live under `tests/`. +- Write a failing test before every production behavior change. +- Do not add a fake stiffness matrix or a test-only solver path. +- Do not introduce empty future modules, a generic registry, MPC, iterative solvers, nonlinear state, or additional element types. +- FESA performs no unit conversion. +- Abaqus inputs may use a flat mesh or one untransformed Part/Assembly/Instance. +- When transverse shear stiffness is omitted, use \(A_{sy}=A_{sz}=5A/6\) and `SCF=0`. +- Reference comparison requests name their quantities and CSV paths; no per-model metadata file is required. +- A pipeline milestone is not a numerically qualified release. +- No new MSVC warnings are allowed. +- The existing Harness contract in `docs/HARNESS.md` and `.agents/skills/harness/SKILL.md` remains unchanged. + +## Environment Audit + +The planning environment currently has: + +- CMake 4.4.0 +- oneMKL CMake package at `C:/Program Files (x86)/Intel/oneAPI/2026.1/lib/cmake/mkl` +- oneTBB CMake package at `C:/Program Files (x86)/Intel/oneAPI/2026.1/lib/cmake/tbb` +- no HDF5 or GoogleTest CMake package found in the standard Program Files trees +- `MSBuild.exe` not currently available on `PATH` + +Task 1 must stop as `blocked` rather than downloading packages if HDF5, GoogleTest, or the MSVC toolchain is still unavailable. + +## Required Research Record + +Before the related production task begins, record the relevant equations, +assumptions, API contracts, and FESA decisions from these sources: + +- K. J. Bathe, *Finite Element Procedures*, 2nd edition: finite-element + discretization, assembly, constraints, and verification. +- T. J. R. Hughes, *The Finite Element Method: Linear Static and Dynamic + Finite Element Analysis*: variational formulation and numerical integration. +- K. J. Bathe and S. Bolourchi, “Large Displacement Analysis of + Three-Dimensional Beam Structures,” 1979: three-dimensional isoparametric + Beam coordinates and transformations. Phase 1 uses only the linearized + subset. +- T. J. R. Hughes, R. L. Taylor, and W. Kanoknukulchai, “A Simple and + Efficient Finite Element for Plate Bending,” 1977: selective reduced + integration rationale. Do not copy its plate kinematics into the Beam + formulation. +- [Abaqus 2024—Choosing a Beam Element](https://docs.software.vt.edu/abaqusv2024/English/SIMACAEELMRefMap/simaelm-c-beamelem.htm): + B31 shear-flexible behavior and slenderness compensation. +- [Abaqus 2024—BEAM GENERAL SECTION](https://docs.software.vt.edu/abaqusv2024/English/SIMACAEKEYRefMap/simakey-r-beamgeneralsection.htm): + supported general-section data and orientation. +- [Intel oneMKL PARDISO reference](https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2025-0/pardiso.html): + matrix type, CSR indexing, phases, checks, and error codes. +- [oneTBB reduction guide](https://uxlfoundation.github.io/oneTBB/main/tbb_userguide/design_patterns/Reduction.html): + deterministic floating-point reduction. +- [HDF5 data model](https://support.hdfgroup.org/documentation/hdf5/latest/_intro_h_d_f5.html): + groups, datasets, dataspaces, and attributes. +- [CMake FindHDF5](https://cmake.org/cmake/help/latest/module/FindHDF5.html): + installed C-library discovery and imported targets. + +`docs/formulation/timoshenko-beam-3d.md`, `docs/HDF5_SCHEMA.md`, and +`docs/VALIDATION.md` must cite the applicable source and state where FESA +intentionally differs. + +## Harness Phase Map + +| Order | Harness phase | Plan tasks | Independent deliverable | +| ---: | --- | --- | --- | +| 0 | `solver-bootstrap` | 1-2 | Reproducible C++20 build, dependency smoke tests, core IDs and diagnostics | +| 1 | `domain-and-input-skeleton` | 3-4 | Flat or single-Instance B31 input becomes an immutable normalized `Domain` | +| 2 | `fem-and-beam-kernel` | 5-6 | Real Timoshenko Beam local stiffness with analytical sanity tests | +| 3 | `equation-and-linear-solve` | 7-8 | Deterministic serial CSR system solved by PARDISO | +| 4 | `results-and-pipeline` | 9-10 | CLI runs one deck end-to-end and writes readable HDF5 | +| 5 | `abaqus-subset-completion` | 11 | Full agreed scoped keyword, set, material, section, load, and BC subset | +| 6 | `deterministic-parallel-assembly` | 12 | oneTBB assembly matches serial output across thread counts | +| 7 | `result-contract-completion` | 13 | Complete self-contained HDF5 schema and Beam result recovery | +| 8 | `beam-reference-qualification` | 14 | Analytical suite and available Abaqus displacement/reaction data pass tolerance | +| 9 | `internal-release` | 15 | Debug/Release validation, 100k-DOF benchmark, install tree, reports | + +Create `phases/index.json`, phase indexes, and step files only after this plan and its phase split are approved. + +## Planned File Map + +```text +CMakeLists.txt root targets and project policies +CMakePresets.json windows-debug/windows-release workflows +cmake/FesaDependencies.cmake installed dependency discovery +.harness/config.json Harness CMake preset selection +include/fesa/core/ IDs, vectors, source locations, diagnostics +include/fesa/model/ immutable semantic entities and Domain +include/fesa/io/abaqus/ deck records, parser, semantic mapper +include/fesa/fem/ quadrature, shape functions, frames, DOFs +include/fesa/elements/beam/ Beam3D2 input, contribution, recovery contract +include/fesa/assembly/ symmetric COO/CSR and assembly +include/fesa/constraints/ essential-BC elimination and reconstruction +include/fesa/solvers/linear/ backend contract and PARDISO adapter +include/fesa/results/ step/frame/field/diagnostic result model +include/fesa/io/hdf5/ schema constants, writer, reader +include/fesa/analysis/ analysis lifecycle and linear-static procedure +include/fesa/validation/ comparison metrics and CSV mapping +src/fesa/ implementations mirroring public modules +src/fesa/cli/main.cpp thin `fesa` command-line executable +tests/unit/ single-module behavior +tests/integration/ public input-to-output path +tests/reference/ FESA HDF5 to golden CSV comparisons +tests/fixtures/ small invalid and valid decks +reference/cantilever beam/ supplied Abaqus input and available golden CSV data +docs/formulation/ signed-off equations and conventions +docs/HDF5_SCHEMA.md versioned output contract +docs/VALIDATION.md benchmark matrix and qualification result +``` + +--- + +### Task 1: CMake, Installed Dependencies, and Test Bootstrap + +**Files:** + +- Create: `CMakeLists.txt` +- Create: `CMakePresets.json` +- Create: `cmake/FesaDependencies.cmake` +- Create: `.harness/config.json` +- Create: `include/fesa/core/version.hpp` +- Create: `src/fesa/core/version.cpp` +- Create: `src/fesa/cli/main.cpp` +- Create: `tests/CMakeLists.txt` +- Create: `tests/unit/core/version_test.cpp` +- Modify: `.gitignore` + +**Interfaces:** + +- Produces: `std::string_view fesa::version() noexcept` +- Produces CMake targets: `fesa_core`, `fesa_cli`, `fesa_unit_tests` +- Produces presets: `windows-debug`, `windows-release` +- Later tasks consume the common warning and include-directory policies. + +- [ ] **Step 1: Verify required installed packages without changing the machine** + +Run: + +```powershell +cmake --version +Get-Command MSBuild.exe +Get-ChildItem "C:\Program Files (x86)\Intel\oneAPI" -Recurse -Filter MKLConfig.cmake +Get-ChildItem "C:\Program Files (x86)\Intel\oneAPI" -Recurse -Filter TBBConfig.cmake +Get-ChildItem "C:\Program Files" -Recurse -Filter hdf5-config.cmake +Get-ChildItem "C:\Program Files" -Recurse -Filter GTestConfig.cmake +``` + +Expected: MSVC, oneMKL, oneTBB, HDF5, and GoogleTest are all discoverable. If any are missing, mark the Harness step `blocked` and name the missing package; do not download it. + +- [ ] **Step 2: Write the failing version test** + +```cpp +#include +#include + +TEST(Version, ReportsPhaseOneSemanticVersion) { + EXPECT_EQ(fesa::version(), "0.1.0"); +} +``` + +- [ ] **Step 3: Add configure files and verify the test fails before implementation** + +`cmake/FesaDependencies.cmake` must set the oneMKL choices before package discovery: + +```cmake +cmake_minimum_required(VERSION 3.30) + +set(MKL_LINK dynamic) +set(MKL_THREADING tbb_thread) +set(MKL_INTERFACE lp64) +find_package(MKL CONFIG REQUIRED) +find_package(TBB CONFIG REQUIRED COMPONENTS tbb) +find_package(HDF5 REQUIRED COMPONENTS C) +find_package(GTest CONFIG REQUIRED) +``` + +Link `MKL::MKL`, `TBB::tbb`, `HDF5::HDF5`, and `GTest::gtest_main` only to targets that use them. Configure and build: +Compile FESA targets with `/W4 /permissive- /EHsc`; do not apply FESA warning +flags to imported targets. + +```powershell +cmake --preset windows-debug +cmake --build --preset windows-debug +``` + +Expected: build fails because `fesa::version()` has no definition. + +- [ ] **Step 4: Implement the minimum version API and thin CLI** + +```cpp +namespace fesa { +std::string_view version() noexcept; +} +``` + +The CLI accepts only `--version` in this task. Any other command prints usage and returns a nonzero exit code. + +- [ ] **Step 5: Run focused and full bootstrap validation** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +.\out\build\windows-debug\Debug\fesa.exe --version +``` + +Expected: one test passes and the CLI prints `0.1.0`. + +- [ ] **Step 6: Commit** + +```powershell +git add CMakeLists.txt CMakePresets.json cmake .harness/config.json include/fesa/core/version.hpp src/fesa/core/version.cpp src/fesa/cli/main.cpp tests/CMakeLists.txt tests/unit/core/version_test.cpp .gitignore +git commit -m "build: bootstrap FESA CMake project" +``` + +--- + +### Task 2: Core IDs, Vectors, Source Locations, and Diagnostics + +**Files:** + +- Create: `include/fesa/core/entity_id.hpp` +- Create: `include/fesa/core/vec3.hpp` +- Create: `include/fesa/core/source_location.hpp` +- Create: `include/fesa/core/diagnostic.hpp` +- Create: `tests/unit/core/entity_id_test.cpp` +- Create: `tests/unit/core/vec3_test.cpp` +- Create: `tests/unit/core/diagnostic_test.cpp` +- Modify: `CMakeLists.txt` +- Modify: `tests/CMakeLists.txt` + +**Interfaces:** + +```cpp +template +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; +}; + +[[nodiscard]] bool is_finite(Vec3 value) noexcept; + +struct SourceLocation final { + std::filesystem::path file; + std::size_t line; + std::size_t column; +}; + +enum class DiagnosticStage { + io, lexical, syntax, semantic, model, equation, solver, results +}; + +struct Diagnostic final { + DiagnosticStage stage; + std::string code; + std::string message; + std::optional source; + std::optional entity_id; +}; +``` + +- [ ] **Step 1: Write failing tests** + +Cover negative/zero entity-ID rejection, typed-ID non-interchangeability at compile time, finite `Vec3` validation, and full diagnostic context preservation. + +- [ ] **Step 2: Run focused tests and confirm compile or assertion failure** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Core" --output-on-failure +``` + +Expected: failure because the core types do not exist. + +- [ ] **Step 3: Implement only the declared core types** + +Use `double` for all Phase 1 real values and `std::int64_t` for external Abaqus IDs. Do not introduce a unit library, generic error monad, matrix class, or logging framework. + +- [ ] **Step 4: Run all tests** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +``` + +Expected: all tests pass with no new MSVC warnings. + +- [ ] **Step 5: Commit** + +```powershell +git add include/fesa/core tests/unit/core CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(core): add typed IDs and diagnostics" +``` + +--- + +### Task 3: Immutable Semantic Domain + +**Files:** + +- Create: `include/fesa/model/ids.hpp` +- Create: `include/fesa/model/entity_origin.hpp` +- Create: `include/fesa/model/node.hpp` +- Create: `include/fesa/model/material.hpp` +- Create: `include/fesa/model/beam_section.hpp` +- Create: `include/fesa/model/beam_element.hpp` +- Create: `include/fesa/model/entity_set.hpp` +- Create: `include/fesa/model/step_definition.hpp` +- Create: `include/fesa/model/domain.hpp` +- Create: `include/fesa/model/domain_builder.hpp` +- Create: `src/fesa/model/domain.cpp` +- Create: `src/fesa/model/domain_builder.cpp` +- Create: `tests/unit/model/domain_builder_test.cpp` +- Modify: root and test CMake files + +**Interfaces:** + +```cpp +using NodeId = EntityId; +using ElementId = EntityId; +using MaterialId = EntityId; +using SectionId = EntityId; + +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 IsotropicElastic final { MaterialId id; std::string name; double young; double poisson; }; +struct BeamSection final { + SectionId id; + std::string name; + double area; + double iy; + double iz; + double torsion_j; + double shear_area_y; + double shear_area_z; + Vec3 orientation; + std::vector> recovery_points; +}; +struct BeamElement final { + ElementId id; + EntityOrigin origin; + std::array nodes; + MaterialId material; + SectionId section; +}; +struct NodeSet final { std::string name; std::vector members; }; +struct ElementSet final { std::string name; std::vector members; }; +struct PrescribedDof final { NodeId node; std::uint8_t dof; double value; }; +struct NodalLoad final { NodeId node; std::array values; }; +struct StepDefinition final { + std::string name; + std::vector prescribed_dofs; + std::vector nodal_loads; +}; + +class Domain final { +public: + [[nodiscard]] std::span nodes() const noexcept; + [[nodiscard]] std::span beam_elements() const noexcept; + [[nodiscard]] const Node& node(NodeId id) const; + [[nodiscard]] const Node& node(const EntityOrigin& origin) const; +}; + +struct DomainBuildResult final { + std::optional domain; + std::vector diagnostics; +}; + +class DomainBuilder final { +public: + void add_node(Node value); + void add_material(IsotropicElastic value); + void add_section(BeamSection value); + void add_beam_element(BeamElement value); + void add_node_set(NodeSet value); + void add_element_set(ElementSet value); + void set_step(StepDefinition value); + [[nodiscard]] DomainBuildResult build() &&; +}; +``` + +`DomainBuilder::build()` returns either one immutable `Domain` or a nonempty +diagnostic list. It must resolve every reference and reject duplicate internal +IDs, duplicate `(instance_name, local_label)` origins, nonfinite values, invalid +material constants, invalid section properties, zero-length elements, missing +assignments, and invalid orientation vectors. Empty Part/Instance names denote +the flat global scope. + +- [ ] **Step 1: Write failing builder tests** + +Write one valid two-node domain test and one test for every rejection listed above. + +- [ ] **Step 2: Run the model tests and confirm failure** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Domain" --output-on-failure +``` + +- [ ] **Step 3: Implement the minimum immutable storage and validation** + +Preserve external IDs and create private dense lookup maps. Do not store equation numbers in `Node` or `BeamElement`. + +- [ ] **Step 4: Run all tests and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +git add include/fesa/model src/fesa/model tests/unit/model CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(model): add immutable beam domain" +``` + +--- + +### Task 4: Minimal Scoped Abaqus Deck to Domain + +**Files:** + +- Create: `include/fesa/io/abaqus/deck_record.hpp` +- Create: `include/fesa/io/abaqus/parser.hpp` +- Create: `include/fesa/io/abaqus/semantic_mapper.hpp` +- Create: `src/fesa/io/abaqus/parser.cpp` +- Create: `src/fesa/io/abaqus/semantic_mapper.cpp` +- Create: `tests/fixtures/abaqus/minimal_cantilever.inp` +- Create: `tests/fixtures/abaqus/minimal_part_instance_cantilever.inp` +- Create: `tests/fixtures/abaqus/unsupported_keyword.inp` +- Create: `tests/unit/io/abaqus/parser_test.cpp` +- Create: `tests/integration/io/minimal_deck_to_domain_test.cpp` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct DeckRecord final { + std::string keyword; + std::map> parameters; + std::vector> data; + SourceLocation source; +}; + +struct ParsedPart final { + std::string name; + std::vector records; + SourceLocation source; +}; + +struct ParsedInstance final { + std::string name; + std::string part_name; + std::vector> transform_data; + SourceLocation source; +}; + +struct ParsedAssembly final { + std::string name; + std::vector instances; + std::vector records; + SourceLocation source; +}; + +struct ParsedDeck final { + std::vector global_records; + std::vector parts; + std::optional assembly; +}; + +struct ParseDeckResult final { + std::optional deck; + std::vector diagnostics; +}; + +[[nodiscard]] ParseDeckResult parse_deck(const std::filesystem::path& path); +[[nodiscard]] DomainBuildResult map_deck_to_domain(const ParsedDeck& deck); +``` + +Each minimal fixture contains two nodes, one B31 element, one material, one +general section, one node set, one element set, one boundary definition, one +concentrated load, and one static step. One fixture is flat; the other contains +one Part, one Assembly, and one untransformed Instance. + +- [ ] **Step 1: Write failing parser and integration tests** + +Tests must call `parse_deck()` and `map_deck_to_domain()`; they may not +construct the `Domain` directly. Assert that both organizations produce +equivalent active analysis entities and that the hierarchical Domain retains +Part/Instance provenance. + +- [ ] **Step 2: Confirm failure** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Abaqus|Deck" --output-on-failure +``` + +- [ ] **Step 3: Implement the smallest case-insensitive keyword parser** + +Support comments, blank lines, comma-separated parameters and data, UTF-8 +input, exact source lines, `*PART/*END PART`, `*ASSEMBLY/*END ASSEMBLY`, and +`*INSTANCE/*END INSTANCE`. In this task, accept only the keywords used by the +two minimal fixtures. Treat `*INCLUDE` and every other keyword as an explicit +unsupported-keyword error. + +- [ ] **Step 4: Implement semantic mapping for the fixture** + +For hierarchical input, require exactly one Assembly and one Instance, reject +nonempty Instance transform data, activate only the referenced Part, and map +the result to the same flat `Domain` used by orphan meshes. When transverse +stiffness is omitted, set \(A_{sy}=A_{sz}=5A/6\) and `SCF=0`; supported +explicit values override the default. + +- [ ] **Step 5: Validate and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +git add include/fesa/io src/fesa/io tests/fixtures tests/unit/io tests/integration/io CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(input): parse minimal Abaqus B31 deck" +``` + +--- + +### Task 5: FEM Primitives and DOF Management + +**Files:** + +- Create: `include/fesa/fem/gauss_rule.hpp` +- Create: `include/fesa/fem/line2_shape.hpp` +- Create: `include/fesa/fem/local_frame.hpp` +- Create: `include/fesa/fem/dof_manager.hpp` +- Create: `src/fesa/fem/local_frame.cpp` +- Create: `src/fesa/fem/dof_manager.cpp` +- Create: `tests/unit/fem/gauss_rule_test.cpp` +- Create: `tests/unit/fem/line2_shape_test.cpp` +- Create: `tests/unit/fem/local_frame_test.cpp` +- Create: `tests/unit/fem/dof_manager_test.cpp` + +**Interfaces:** + +```cpp +struct GaussPoint1D final { double xi; double weight; }; +[[nodiscard]] std::array gauss_rule_1(); +[[nodiscard]] std::array gauss_rule_2(); +[[nodiscard]] std::array line2_shape(double xi); +[[nodiscard]] std::array line2_shape_derivative(); + +struct LocalFrame final { Vec3 ex; Vec3 ey; Vec3 ez; double length; }; +[[nodiscard]] LocalFrame make_beam_frame(Vec3 first, Vec3 second, Vec3 orientation); + +class DofManager final { +public: + explicit DofManager(const Domain& domain); + [[nodiscard]] std::size_t full_dof_count() const noexcept; + [[nodiscard]] std::array beam_dofs(ElementId id) const; +}; +``` + +- [ ] **Step 1: Write failing mathematical invariant tests** + +Test Gauss exactness through degree 3, shape-function partition of unity, derivative sum zero, right-handed orthonormal frames, nearly parallel orientation rejection, stable external-ID ordering, and 12-DOF element maps. + +- [ ] **Step 2: Confirm failure, implement, and rerun** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Fem|Dof|Frame" --output-on-failure +``` + +- [ ] **Step 3: Run full validation and commit** + +```powershell +ctest --preset windows-debug --output-on-failure +git add include/fesa/fem src/fesa/fem tests/unit/fem CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(fem): add quadrature frames and DOF mapping" +``` + +--- + +### Task 6: Minimal Real Timoshenko Beam Kernel + +**Files:** + +- Create: `docs/formulation/timoshenko-beam-3d.md` +- Create: `include/fesa/elements/beam/beam3d2.hpp` +- Create: `src/fesa/elements/beam/beam3d2.cpp` +- Create: `tests/unit/elements/beam3d2_test.cpp` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct Beam3D2Input final { + std::array coordinates; + IsotropicElastic material; + BeamSection section; +}; + +struct Beam3D2Contribution final { + std::array stiffness; + std::array equivalent_load; + LocalFrame frame; +}; + +[[nodiscard]] Beam3D2Contribution evaluate_beam3d2(const Beam3D2Input& input); +``` + +- [ ] **Step 1: Write and review the formulation document before production code** + +The document must define DOF order, local axes, strain measures, constitutive diagonal, Jacobian, transformation, Gauss rules, matrix storage order, force/moment signs, and reference sources. It must show that axial/bending/torsion use two points and shear uses one point. + +- [ ] **Step 2: Write failing kernel tests** + +Test symmetry, six rigid-body modes, positive strain energy for non-rigid modes, analytical axial stiffness \(EA/L\), analytical torsional stiffness \(GJ/L\), coordinate-rotation invariance, and finite values. + +- [ ] **Step 3: Confirm the tests fail** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Beam3D2" --output-on-failure +``` + +- [ ] **Step 4: Implement the minimum kernel from the signed-off equations** + +Use fixed-size `std::array` storage and small explicit loops. Do not add a dynamic matrix abstraction or copy Abaqus slenderness compensation. + +- [ ] **Step 5: Validate and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +git add docs/formulation include/fesa/elements src/fesa/elements tests/unit/elements CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(elements): add 3D Timoshenko beam kernel" +``` + +--- + +### Task 7: Deterministic Serial Assembly and Essential BC Elimination + +**Files:** + +- Create: `include/fesa/assembly/symmetric_coo.hpp` +- Create: `include/fesa/assembly/symmetric_csr.hpp` +- Create: `include/fesa/assembly/assembler.hpp` +- Create: `src/fesa/assembly/assembler.cpp` +- Create: `include/fesa/constraints/essential_bc.hpp` +- Create: `src/fesa/constraints/essential_bc.cpp` +- Create: `tests/unit/assembly/assembler_test.cpp` +- Create: `tests/unit/constraints/essential_bc_test.cpp` + +**Interfaces:** + +```cpp +struct CooEntry final { + std::size_t row; + std::size_t column; + ElementId source_element; + std::size_t local_order; + double value; +}; + +struct SymmetricCsr final { + std::vector row_offsets; + std::vector column_indices; + std::vector values; +}; + +struct EquationSystem final { + SymmetricCsr stiffness; + std::vector load; +}; + +[[nodiscard]] EquationSystem assemble_serial( + const Domain& domain, + const DofManager& dofs); + +struct ReducedSystem final { + SymmetricCsr stiffness; + std::vector rhs; + std::vector free_dofs; + std::vector prescribed_full_values; +}; +``` + +- [ ] **Step 1: Write failing assembly tests** + +Cover a one-element matrix, a two-element shared-node chain, stable sort/reduction order, upper-triangle storage with every diagonal present, nonzero prescribed values, reduced RHS correction, and full-vector reconstruction. + +- [ ] **Step 2: Confirm failure and implement serial baseline** + +Sort COO entries by `(row, column, source_element, local_order)` and then sum. This serial result is the oracle for Task 12. + +- [ ] **Step 3: Verify and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Assembly|EssentialBc" --output-on-failure +ctest --preset windows-debug --output-on-failure +git add include/fesa/assembly src/fesa/assembly include/fesa/constraints src/fesa/constraints tests/unit/assembly tests/unit/constraints CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(assembly): assemble and constrain beam systems" +``` + +--- + +### Task 8: MKL PARDISO Linear Solver Adapter + +**Files:** + +- Create: `include/fesa/solvers/linear/linear_solver.hpp` +- Create: `include/fesa/solvers/linear/pardiso_solver.hpp` +- Create: `src/fesa/solvers/linear/pardiso_solver.cpp` +- Create: `tests/unit/solvers/pardiso_solver_test.cpp` +- Modify: `cmake/FesaDependencies.cmake` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct LinearSolveResult final { + std::vector solution; + double relative_residual; + std::vector diagnostics; +}; + +class LinearSolver { +public: + virtual ~LinearSolver() = default; + [[nodiscard]] virtual LinearSolveResult solve( + const SymmetricCsr& matrix, + std::span rhs) = 0; +}; + +class PardisoLinearSolver final : public LinearSolver { +public: + PardisoLinearSolver(); + ~PardisoLinearSolver() override; + PardisoLinearSolver(const PardisoLinearSolver&) = delete; + PardisoLinearSolver& operator=(const PardisoLinearSolver&) = delete; + [[nodiscard]] LinearSolveResult solve( + const SymmetricCsr& matrix, + std::span rhs) override; +}; +``` + +- [ ] **Step 1: Write failing adapter tests** + +Use a hand-calculated 3x3 SPD system, multiple RHS calls on one adapter, invalid CSR, dimension mismatch, and a singular matrix. Assert `mtype=2`, LP64-compatible index checks, 0-based indexing, and residual reporting through observable behavior. + +- [ ] **Step 2: Confirm failure** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Pardiso" --output-on-failure +``` + +- [ ] **Step 3: Implement RAII PARDISO phases** + +Set `iparm[34]=1` for zero-based indexing and enable the matrix checker. Run symbolic analysis, numerical factorization, solve, and release. Convert every MKL error to `DiagnosticStage::solver`. + +- [ ] **Step 4: Validate and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +git add include/fesa/solvers src/fesa/solvers tests/unit/solvers cmake/FesaDependencies.cmake CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(solver): add PARDISO linear backend" +``` + +--- + +### Task 9: Result Model and Minimal HDF5 Round Trip + +**Files:** + +- Create: `docs/HDF5_SCHEMA.md` +- Create: `include/fesa/results/result_database.hpp` +- Create: `include/fesa/io/hdf5/schema.hpp` +- Create: `include/fesa/io/hdf5/writer.hpp` +- Create: `include/fesa/io/hdf5/reader.hpp` +- Create: `src/fesa/io/hdf5/writer.cpp` +- Create: `src/fesa/io/hdf5/reader.cpp` +- Create: `tests/unit/io/hdf5_round_trip_test.cpp` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct NodalFrame final { + std::vector node_ids; + std::vector> displacement; + std::vector> reaction; +}; + +struct ResultFrame final { + double step_time; + NodalFrame nodal; + std::vector diagnostics; +}; + +struct ResultStep final { + std::string name; + std::vector frames; +}; + +struct ResultDatabase final { + std::string schema_version; + std::vector steps; +}; + +struct Hdf5ReadResult final { + std::optional database; + std::vector diagnostics; +}; + +[[nodiscard]] std::vector write_hdf5( + const std::filesystem::path&, + const Domain&, + const ResultDatabase&); +[[nodiscard]] Hdf5ReadResult read_hdf5_results(const std::filesystem::path&); +``` + +- [ ] **Step 1: Write schema version `1.0.0` before writer code** + +Define exact group paths, dataset ranks, scalar types, dense ID mappings, +Part/Instance/local-label origins, coordinate-system attributes, applied +transverse-shear values and their input/default source, required HDF5 metadata, +and compatibility rules. + +- [ ] **Step 2: Write a failing round-trip test** + +Use a two-node hierarchical-origin `Domain` and one result frame. Reopen +through the FESA reader and compare every stored value, origin mapping, +transverse-shear source, and schema attribute. + +- [ ] **Step 3: Confirm failure and implement minimum C-API RAII wrappers** + +Do not use global HDF5 handles. Convert every failing HDF5 call into a results-stage diagnostic or exception caught at the adapter boundary. + +- [ ] **Step 4: Validate with tests and HDF5 tools** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Hdf5" --output-on-failure +h5ls -r .\out\build\windows-debug\Testing\Temporary\fesa-round-trip.h5 +ctest --preset windows-debug --output-on-failure +``` + +- [ ] **Step 5: Commit** + +```powershell +git add docs/HDF5_SCHEMA.md include/fesa/results include/fesa/io/hdf5 src/fesa/io/hdf5 tests/unit/io CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(results): add versioned HDF5 result adapter" +``` + +--- + +### Task 10: Linear Static Analysis and End-to-End CLI Slice + +**Files:** + +- Create: `include/fesa/analysis/analysis.hpp` +- Create: `include/fesa/analysis/linear_static_analysis.hpp` +- Create: `src/fesa/analysis/analysis.cpp` +- Create: `src/fesa/analysis/linear_static_analysis.cpp` +- Create: `include/fesa/analysis/run_solver.hpp` +- Create: `src/fesa/analysis/run_solver.cpp` +- Modify: `src/fesa/cli/main.cpp` +- Create: `tests/integration/pipeline/minimal_cantilever_test.cpp` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct AnalysisRequest final { + std::filesystem::path input_path; + std::filesystem::path output_path; +}; + +struct AnalysisRunResult final { + bool succeeded; + std::vector diagnostics; +}; + +[[nodiscard]] AnalysisRunResult run_solver(const AnalysisRequest& request); +``` + +The CLI contract is: + +```text +fesa solve --output +fesa --version +``` + +- [ ] **Step 1: Write the failing end-to-end test** + +Invoke only `run_solver()` or the CLI with `tests/fixtures/abaqus/minimal_cantilever.inp`, then use the public HDF5 reader to assert node IDs, finite displacement, equilibrium residual, and result paths. + +- [ ] **Step 2: Confirm the pipeline test fails** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "MinimalCantileverPipeline" --output-on-failure +``` + +- [ ] **Step 3: Implement the analysis lifecycle** + +Connect parser, `Domain`, `DofManager`, Beam kernel, serial assembly, essential BC, PARDISO, full-vector reconstruction, reaction recovery, result model, and HDF5 writer. Keep CLI parsing out of `fesa_core`. + +- [ ] **Step 4: Validate the vertical slice** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --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 +``` + +Expected: full pipeline succeeds. Do not label the Beam numerically qualified yet. + +- [ ] **Step 5: Commit** + +```powershell +git add include/fesa/analysis src/fesa/analysis src/fesa/cli/main.cpp tests/integration/pipeline CMakeLists.txt tests/CMakeLists.txt +git commit -m "feat(analysis): connect linear static pipeline" +``` + +--- + +### Task 11: Complete the Agreed Abaqus Input Subset + +**Files:** + +- Modify: Abaqus parser and mapper files from Task 4 +- Create: `tests/unit/io/abaqus/set_resolution_test.cpp` +- Create: `tests/unit/io/abaqus/scope_resolution_test.cpp` +- Create: `tests/unit/io/abaqus/semantic_validation_test.cpp` +- Create: `tests/integration/io/multiple_properties_test.cpp` +- Create: `tests/integration/io/supplied_cantilever_to_domain_test.cpp` +- Create fixtures under: `tests/fixtures/abaqus/valid/` +- Create fixtures under: `tests/fixtures/abaqus/invalid/` +- Create: `docs/ABAQUS_INPUT_SUBSET.md` + +**Interfaces:** + +- Existing parser and mapper signatures remain unchanged. +- `docs/ABAQUS_INPUT_SUBSET.md` becomes the normative keyword/parameter/data-line contract. + +- [ ] **Step 1: Write the contract document and failing fixture matrix** + +Cover `*NODE`, B31 `*ELEMENT`, `*PART/*END PART`, +`*ASSEMBLY/*END ASSEMBLY`, `*INSTANCE/*END INSTANCE`, `*NSET`, `*ELSET`, +explicit members, `GENERATE`, nested set references, `INSTANCE=`, +`*MATERIAL`, `*ELASTIC`, general Beam section, optional transverse shear +stiffness, `*BOUNDARY`, `*CLOAD`, and one static step. Document +`*HEADING`, `*PREPRINT`, `*RESTART`, and `*OUTPUT` as recognized no-op +directives. + +- [ ] **Step 2: Add invalid tests before parser changes** + +Cover duplicate IDs and origins, missing references, set cycles, invalid +ranges, multiple section assignments, missing material, conflicting prescribed +values, unsupported options, `*INCLUDE`, multiple Assemblies, multiple +Instances, Instance translation/rotation data, instance-local mesh changes, +mixed flat/hierarchical meshes, nonzero `SCF`, multiple steps, and source-line +accuracy. + +- [ ] **Step 3: Run and confirm the new tests fail** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Abaqus|SetResolution|MultipleProperties" --output-on-failure +``` + +- [ ] **Step 4: Implement only the documented subset** + +Resolve Part and Assembly scopes before normalizing only the active Part and +Instance. Resolve nested sets with explicit cycle detection and canonical +sorted-unique membership. Recognized no-op directives must be consumed +deliberately; do not add a general ignore-unknown path. Verify the supplied +`reference/cantilever beam/cantilever beam.inp` maps to the expected 11 nodes, +10 elements, one active material/section, six fixed DOFs, and one nodal load. + +- [ ] **Step 5: Validate and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +git add docs/ABAQUS_INPUT_SUBSET.md include/fesa/io/abaqus src/fesa/io/abaqus tests/fixtures/abaqus tests/unit/io/abaqus tests/integration/io +git commit -m "feat(input): complete Phase 1 Abaqus subset" +``` + +--- + +### Task 12: Deterministic oneTBB Assembly + +**Files:** + +- Modify: `include/fesa/assembly/assembler.hpp` +- Modify: `src/fesa/assembly/assembler.cpp` +- Create: `tests/unit/assembly/parallel_assembler_test.cpp` +- Create: `tests/integration/assembly/thread_count_determinism_test.cpp` +- Create: `tests/performance/assembly_benchmark.cpp` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct AssemblyOptions final { + std::size_t max_threads; + std::size_t grain_size; +}; + +[[nodiscard]] EquationSystem assemble_parallel( + const Domain& domain, + const DofManager& dofs, + AssemblyOptions options); +``` + +- [ ] **Step 1: Write failing serial-versus-parallel tests** + +Generate fixed chain and branched Beam domains. Compare CSR row offsets and column indices exactly and values bit-for-bit for thread counts 1, 2, and the available concurrency. + +- [ ] **Step 2: Confirm failure** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "ParallelAssembly|ThreadCount" --output-on-failure +``` + +- [ ] **Step 3: Implement parallel element evaluation with deterministic merge** + +Use oneTBB for independent element evaluation. Each contribution retains `(row, column, element ID, local order)`. Sort and reduce in the same order as the serial oracle. Do not perform concurrent unordered writes to CSR values. + +- [ ] **Step 4: Validate correctness before measuring performance** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug --output-on-failure +.\out\build\windows-debug\Debug\fesa_assembly_benchmark.exe +``` + +Record timings without asserting a speedup in unit tests. + +- [ ] **Step 5: Commit** + +```powershell +git add include/fesa/assembly src/fesa/assembly tests/unit/assembly tests/integration/assembly tests/performance CMakeLists.txt tests/CMakeLists.txt +git commit -m "perf(assembly): add deterministic TBB assembly" +``` + +--- + +### Task 13: Complete Beam Recovery and Self-Contained HDF5 Contract + +**Files:** + +- Modify: `include/fesa/elements/beam/beam3d2.hpp` +- Modify: `src/fesa/elements/beam/beam3d2.cpp` +- Modify: result-model and HDF5 files from Task 9 +- Modify: `docs/HDF5_SCHEMA.md` +- Create: `tests/unit/elements/beam3d2_recovery_test.cpp` +- Create: `tests/integration/results/self_contained_hdf5_test.cpp` + +**Interfaces:** + +```cpp +struct BeamSectionResult final { + double xi; + NodeId end_node; + std::array section_strain; + std::array section_force; + double centroid_sigma_xx; + std::vector sigma_xx; +}; + +[[nodiscard]] std::vector recover_beam3d2( + const Beam3D2Input& input, + std::span element_displacement, + std::span> recovery_points); +``` + +- [ ] **Step 1: Write failing recovery tests** + +Cover pure axial force, pure torsion, bending about each principal axis, +combined axial/biaxial bending, force sign at both element ends, +`centroid_sigma_xx=N/A`, and recovery-point ordering. + +- [ ] **Step 2: Write the failing self-contained-file test** + +Reopen one result file and reconstruct node coordinates, connectivity, +Part/Instance origins, sets, material, section, applied shear values and their +source, step, solver settings, local frame, nodal fields, element section +fields including centroid stress, ID maps, and diagnostics. + +- [ ] **Step 3: Implement recovery and schema additions** + +Do not output point shear or point torsional stress. Store section shear resultants and torsional moment as generalized quantities. + +- [ ] **Step 4: Validate and commit** + +```powershell +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Beam3D2Recovery|SelfContainedHdf5" --output-on-failure +ctest --preset windows-debug --output-on-failure +git add include/fesa/elements src/fesa/elements include/fesa/results include/fesa/io/hdf5 src/fesa/io/hdf5 docs/HDF5_SCHEMA.md tests/unit/elements tests/integration/results +git commit -m "feat(results): recover and store beam section results" +``` + +--- + +### Task 14: Analytical and Available Abaqus 2024 Qualification + +**Files:** + +- Create: `include/fesa/validation/comparison.hpp` +- Create: `include/fesa/validation/reference_csv.hpp` +- Create: `src/fesa/validation/comparison.cpp` +- Create: `src/fesa/validation/reference_csv.cpp` +- Create: `src/fesa/validation/reference_compare_main.cpp` +- Create: `tests/unit/validation/comparison_test.cpp` +- Create: `tests/unit/validation/reference_csv_test.cpp` +- Create: `tests/fixtures/reference/internalforces.csv` +- Create: `tests/fixtures/reference/stresses.csv` +- Create: `tests/reference/CMakeLists.txt` +- Create: `tests/reference/cantilever_reference_test.cpp` +- Create: `docs/VALIDATION.md` +- Modify: CMake files + +**Interfaces:** + +```cpp +struct Tolerance final { + double relative; + double absolute_scale; +}; + +enum class ReferenceQuantity { + displacement, + reaction, + internal_force, + centroid_stress +}; + +struct ResultPosition final { + std::string instance_name; + std::int64_t entity_label; + std::optional end_node_label; +}; + +struct ComparisonSample final { + ReferenceQuantity quantity; + ResultPosition position; + std::vector reference; + std::vector actual; + Tolerance tolerance; +}; + +struct ReferenceRow final { + ReferenceQuantity quantity; + ResultPosition position; + std::vector values; +}; + +struct ComparisonReport final { + bool passed; + double maximum_normalized_error; + std::vector failures; +}; + +[[nodiscard]] ComparisonReport compare_samples( + std::span samples); + +struct ReferenceCsvReadResult final { + std::vector rows; + std::vector diagnostics; +}; + +[[nodiscard]] ReferenceCsvReadResult read_reference_csv( + ReferenceQuantity quantity, + const std::filesystem::path& path, + std::string_view single_instance_name, + Tolerance tolerance); +``` + +For each scalar component, define +\[ +e_n=\frac{|a-r|}{a_\mathrm{scale}+r_\mathrm{tol}|r|} +\] +and pass only when \(e_n\leq1\). Reject nonfinite inputs before computing the +metric. + +- [ ] **Step 1: Write failing metric and entity-matching tests** + +Test the normalized error, near-zero absolute scale, nonfinite values, +duplicate positions, unknown entities, invalid element-node pairs, and +component-count mismatches. + +- [ ] **Step 2: Write failing CSV adapter tests for all four quantities** + +Accept the supplied displacement/reaction headers after trimming whitespace. +For a single Instance, allow the `Part Instance Name` column to be absent. +Use these exact element schemas: + +```text +Part Instance Name, Element Label, Node Label, +SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3 + +Part Instance Name, Element Label, Node Label, Sxx +``` + +Map `SF1,SF2,SF3,SM1,SM2,SM3` to \(N,V_y,V_z,T,M_y,M_z\). +Compare `Sxx` with the element-end section-centroid value \(N/A\). The +synthetic fixtures exercise both element result schemas before real files +exist. + +- [ ] **Step 3: Write the failing supplied-cantilever reference test** + +Run FESA through the public parser, analysis, and HDF5 reader. Create an +explicit request selecting only: + +```text +reference/cantilever beam/cantilever beam displacements.csv +reference/cantilever beam/cantilever beam reactions.csv +``` + +Use relative tolerance \(10^{-5}\) and test-registered absolute scales. Do not +look for metadata, internal-force CSV, or stress CSV in this reference test. +The equivalent command-line contract is: + +```powershell +fesa-reference-compare ` + --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 +``` + +- [ ] **Step 4: Confirm failures before numerical corrections** + +```powershell +cmake --preset windows-debug +cmake --build --preset windows-debug +ctest --preset windows-debug -R "Reference" --output-on-failure +``` + +- [ ] **Step 5: Correct only evidenced formulation or recovery defects** + +For every change, add or tighten the smallest analytical test that reproduces +the discrepancy. Do not widen tolerance to hide a defect. Record justified +tolerance differences in `docs/VALIDATION.md` and the CTest registration. + +- [ ] **Step 6: Run the full qualification suite** + +```powershell +ctest --preset windows-debug --output-on-failure +ctest --preset windows-debug -R "Reference" --output-on-failure +``` + +Expected: all analytical, physics-sanity, pipeline, deterministic-parallel, +CSV-adapter, and available Abaqus displacement/reaction comparisons pass. +Do not claim Abaqus qualification of element internal force or stress until +those CSV files are supplied and selected. + +- [ ] **Step 7: Complete the validation report and commit** + +`docs/VALIDATION.md` must list each benchmark, analytical/reference source, +Abaqus configuration, selected quantities, tolerance, maximum observed error, +and disposition. It must distinguish synthetic adapter coverage from +Abaqus-backed quantity qualification. + +```powershell +git add include/fesa/validation src/fesa/validation tests/unit/validation tests/fixtures/reference tests/reference docs/VALIDATION.md CMakeLists.txt tests/CMakeLists.txt +git commit -m "test(validation): qualify Beam solver against Abaqus" +``` + +--- + +### Task 15: Internal Release Gate + +**Files:** + +- Create: `cmake/install.cmake` +- Create: `cmake/FESAConfig.cmake.in` +- Create: `docs/BUILDING.md` +- Create: `docs/INPUT_FORMAT.md` +- Create: `docs/RELEASE_CHECKLIST.md` +- Create: `tests/performance/phase1_scale_benchmark.cpp` +- Modify: root CMake files +- Modify: `docs/VALIDATION.md` + +**Interfaces:** + +- Produces install tree containing `fesa.exe`, the static library, public headers, required runtime DLL inventory, example input, schema, and validation report. +- No public ABI compatibility promise is made for Phase 1. + +- [ ] **Step 1: Write the release checklist before packaging changes** + +Include environment versions, Debug/Release builds, zero-warning requirement, +CTest count, selected reference quantities, synthetic four-quantity adapter +coverage, HDF5 inspection, 100k-DOF memory/time measurement, runtime DLL +inventory, example run, and clean install-tree smoke test. + +- [ ] **Step 2: Add a failing install-tree smoke test** + +The test configures a small consumer against installed headers and the static library, runs `fesa --version`, solves the example, and opens its HDF5 output. + +- [ ] **Step 3: Implement CMake install rules** + +Use `cmake --install`; do not add an installer, registry writes, package download, or external-customer SDK promise. + +- [ ] **Step 4: Run Debug, Release, performance, and install validation** + +```powershell +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 +``` + +Expected: all tests pass, no new MSVC warnings exist, the benchmark completes within documented resources, and the clean install-tree smoke test passes. + +- [ ] **Step 5: Verify every PRD release criterion** + +Map every item in `docs/PRD.md` section 8 to fresh command output or a validation-report entry. Stop the release if any item lacks evidence. + +- [ ] **Step 6: Commit** + +```powershell +git add cmake/install.cmake docs/BUILDING.md docs/INPUT_FORMAT.md docs/RELEASE_CHECKLIST.md docs/VALIDATION.md tests/performance CMakeLists.txt +git commit -m "chore(release): prepare FESA Phase 1 internal package" +``` + +## Plan Execution Gate + +Before implementation: + +1. Review and approve this phase split. +2. Use the Harness skill to draft `phases/index.json`, each phase index, and self-contained step files. +3. Review the phase-file draft before creating it. +4. Execute one Harness phase at a time. +5. Do not begin the next phase until its tests, review, and acceptance commands pass. diff --git a/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md b/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md new file mode 100644 index 0000000..dd261a8 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-abaqus-assembly-reference-design.md @@ -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. diff --git a/phases/abaqus-subset-completion/index.json b/phases/abaqus-subset-completion/index.json new file mode 100644 index 0000000..29c604c --- /dev/null +++ b/phases/abaqus-subset-completion/index.json @@ -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" + } + ] +} diff --git a/phases/abaqus-subset-completion/step0.md b/phases/abaqus-subset-completion/step0.md new file mode 100644 index 0000000..1ffd7f9 --- /dev/null +++ b/phases/abaqus-subset-completion/step0.md @@ -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를 보존해야 한다. diff --git a/phases/abaqus-subset-completion/step1.md b/phases/abaqus-subset-completion/step1.md new file mode 100644 index 0000000..a52ad22 --- /dev/null +++ b/phases/abaqus-subset-completion/step1.md @@ -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 sorted_unique_labels; +}; +struct SetResolutionResult final { + std::vector sets; + std::vector 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 범위다. diff --git a/phases/abaqus-subset-completion/step2.md b/phases/abaqus-subset-completion/step2.md new file mode 100644 index 0000000..0e397b6 --- /dev/null +++ b/phases/abaqus-subset-completion/step2.md @@ -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 part_records; + std::span 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을 만들지 마라. 이유: 실제 두 번째 구현이 없다. diff --git a/phases/abaqus-subset-completion/step3.md b/phases/abaqus-subset-completion/step3.md new file mode 100644 index 0000000..4f917a0 --- /dev/null +++ b/phases/abaqus-subset-completion/step3.md @@ -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 입력 계약이다. diff --git a/phases/abaqus-subset-completion/step4.md b/phases/abaqus-subset-completion/step4.md new file mode 100644 index 0000000..aec6853 --- /dev/null +++ b/phases/abaqus-subset-completion/step4.md @@ -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하지 마라. 이유: 검증 원본을 보존해야 한다. diff --git a/phases/beam-reference-qualification/index.json b/phases/beam-reference-qualification/index.json new file mode 100644 index 0000000..3953edb --- /dev/null +++ b/phases/beam-reference-qualification/index.json @@ -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" + } + ] +} diff --git a/phases/beam-reference-qualification/step0.md b/phases/beam-reference-qualification/step0.md new file mode 100644 index 0000000..0d621af --- /dev/null +++ b/phases/beam-reference-qualification/step0.md @@ -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 end_node_label; +}; +struct ComparisonSample final { + ReferenceQuantity quantity; + ResultPosition position; + std::vector reference; + std::vector actual; + Tolerance tolerance; +}; +struct ComparisonReport final { + bool passed; + double maximum_normalized_error; + std::vector failures; +}; +[[nodiscard]] ComparisonReport compare_samples( + std::span); +``` + +각 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 경계다. diff --git a/phases/beam-reference-qualification/step1.md b/phases/beam-reference-qualification/step1.md new file mode 100644 index 0000000..2d13c33 --- /dev/null +++ b/phases/beam-reference-qualification/step1.md @@ -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 values; +}; +struct ReferenceCsvReadResult final { + std::vector rows; + std::vector 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 구현을 생략하지 마라. 이유: 필수 루틴이다. diff --git a/phases/beam-reference-qualification/step2.md b/phases/beam-reference-qualification/step2.md new file mode 100644 index 0000000..98f4c7a --- /dev/null +++ b/phases/beam-reference-qualification/step2.md @@ -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 검증이다. diff --git a/phases/beam-reference-qualification/step3.md b/phases/beam-reference-qualification/step3.md new file mode 100644 index 0000000..af1cc4d --- /dev/null +++ b/phases/beam-reference-qualification/step3.md @@ -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를 약화한다. diff --git a/phases/deterministic-parallel-assembly/index.json b/phases/deterministic-parallel-assembly/index.json new file mode 100644 index 0000000..6e16fb5 --- /dev/null +++ b/phases/deterministic-parallel-assembly/index.json @@ -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" + } + ] +} diff --git a/phases/deterministic-parallel-assembly/step0.md b/phases/deterministic-parallel-assembly/step0.md new file mode 100644 index 0000000..84b1e5c --- /dev/null +++ b/phases/deterministic-parallel-assembly/step0.md @@ -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 canonicalize_contributions( + std::span); +[[nodiscard]] SymmetricCsr merge_contributions( + std::size_t order, + std::span 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을 준비하지 마라. 이유: 결정성을 깨뜨린다. diff --git a/phases/deterministic-parallel-assembly/step1.md b/phases/deterministic-parallel-assembly/step1.md new file mode 100644 index 0000000..ebdc9fa --- /dev/null +++ b/phases/deterministic-parallel-assembly/step1.md @@ -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를 완화하지 마라. 이유: 검증 결정성이 우선이다. diff --git a/phases/deterministic-parallel-assembly/step2.md b/phases/deterministic-parallel-assembly/step2.md new file mode 100644 index 0000000..8139fd3 --- /dev/null +++ b/phases/deterministic-parallel-assembly/step2.md @@ -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 성능 목표를 임의로 만들지 마라. 이유: 문서화된 측정만 요구된다. diff --git a/phases/domain-and-input-skeleton/index.json b/phases/domain-and-input-skeleton/index.json new file mode 100644 index 0000000..55802e9 --- /dev/null +++ b/phases/domain-and-input-skeleton/index.json @@ -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" + } + ] +} diff --git a/phases/domain-and-input-skeleton/step0.md b/phases/domain-and-input-skeleton/step0.md new file mode 100644 index 0000000..d641b69 --- /dev/null +++ b/phases/domain-and-input-skeleton/step0.md @@ -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 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 책임이다. diff --git a/phases/domain-and-input-skeleton/step1.md b/phases/domain-and-input-skeleton/step1.md new file mode 100644 index 0000000..fccab55 --- /dev/null +++ b/phases/domain-and-input-skeleton/step1.md @@ -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; + std::vector 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를 추가하지 마라. 이유: 해석 중 모델 불변성을 깨뜨린다. diff --git a/phases/domain-and-input-skeleton/step2.md b/phases/domain-and-input-skeleton/step2.md new file mode 100644 index 0000000..bea7ad8 --- /dev/null +++ b/phases/domain-and-input-skeleton/step2.md @@ -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> parameters; + std::vector> data; + SourceLocation source; +}; +struct ParsedPart final { + std::string name; + std::vector records; + SourceLocation source; +}; +struct ParsedInstance final { + std::string name; + std::string part_name; + std::vector> transform_data; + SourceLocation source; +}; +struct ParsedAssembly final { + std::string name; + std::vector instances; + std::vector records; + SourceLocation source; +}; +struct ParsedDeck final { + std::vector global_records; + std::vector parts; + std::optional 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 정보로 보존한다. diff --git a/phases/domain-and-input-skeleton/step3.md b/phases/domain-and-input-skeleton/step3.md new file mode 100644 index 0000000..62f3e4c --- /dev/null +++ b/phases/domain-and-input-skeleton/step3.md @@ -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 경로를 우회한다. diff --git a/phases/equation-and-linear-solve/index.json b/phases/equation-and-linear-solve/index.json new file mode 100644 index 0000000..dd36b3b --- /dev/null +++ b/phases/equation-and-linear-solve/index.json @@ -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" + } + ] +} diff --git a/phases/equation-and-linear-solve/step0.md b/phases/equation-and-linear-solve/step0.md new file mode 100644 index 0000000..72f5227 --- /dev/null +++ b/phases/equation-and-linear-solve/step0.md @@ -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 row_offsets; + std::vector column_indices; + std::vector values; +}; +struct EquationSystem final { + SymmetricCsr stiffness; + std::vector 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에 복제하지 마라. 이유: 모듈 경계를 깨뜨린다. diff --git a/phases/equation-and-linear-solve/step1.md b/phases/equation-and-linear-solve/step1.md new file mode 100644 index 0000000..cc8d8aa --- /dev/null +++ b/phases/equation-and-linear-solve/step1.md @@ -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 force; + std::vector free_to_full; + std::vector prescribed_full; +}; +[[nodiscard]] ConstraintResult eliminate_essential_bcs( + const EquationSystem& original, + const DofManager& dofs, + std::span prescribed); +[[nodiscard]] std::vector recover_reaction( + const EquationSystem& original, + std::span 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를 추가하지 마라. 이유: 범위 밖이다. diff --git a/phases/equation-and-linear-solve/step2.md b/phases/equation-and-linear-solve/step2.md new file mode 100644 index 0000000..fe35327 --- /dev/null +++ b/phases/equation-and-linear-solve/step2.md @@ -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 solution; + double relative_residual; + std::vector diagnostics; +}; +class LinearSolver { +public: + virtual ~LinearSolver() = default; + [[nodiscard]] virtual LinearSolveResult solve( + const SymmetricCsr&, + std::span rhs) = 0; +}; +class PardisoLinearSolver final : public LinearSolver { +public: + PardisoLinearSolver(); + ~PardisoLinearSolver() override; + [[nodiscard]] LinearSolveResult solve( + const SymmetricCsr&, + std::span) 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 정책 위반이다. diff --git a/phases/fem-and-beam-kernel/index.json b/phases/fem-and-beam-kernel/index.json new file mode 100644 index 0000000..a04f542 --- /dev/null +++ b/phases/fem-and-beam-kernel/index.json @@ -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" + } + ] +} diff --git a/phases/fem-and-beam-kernel/step0.md b/phases/fem-and-beam-kernel/step0.md new file mode 100644 index 0000000..9571299 --- /dev/null +++ b/phases/fem-and-beam-kernel/step0.md @@ -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 gauss_rule_1d(int order); +[[nodiscard]] std::array line2_shape(double xi); +[[nodiscard]] std::array 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 오류를 숨긴다. diff --git a/phases/fem-and-beam-kernel/step1.md b/phases/fem-and-beam-kernel/step1.md new file mode 100644 index 0000000..8963565 --- /dev/null +++ b/phases/fem-and-beam-kernel/step1.md @@ -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 equation(DofAddress) const; + [[nodiscard]] std::array element_full_dofs( + const BeamElement&) const; + [[nodiscard]] std::vector reconstruct_full( + std::span 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 범위 밖이다. diff --git a/phases/fem-and-beam-kernel/step2.md b/phases/fem-and-beam-kernel/step2.md new file mode 100644 index 0000000..266cf34 --- /dev/null +++ b/phases/fem-and-beam-kernel/step2.md @@ -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 frame; + std::vector 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의 책임이다. diff --git a/phases/fem-and-beam-kernel/step3.md b/phases/fem-and-beam-kernel/step3.md new file mode 100644 index 0000000..9d1081d --- /dev/null +++ b/phases/fem-and-beam-kernel/step3.md @@ -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 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 밖이다. diff --git a/phases/index.json b/phases/index.json new file mode 100644 index 0000000..bd97b5c --- /dev/null +++ b/phases/index.json @@ -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" + } + ] +} diff --git a/phases/internal-release/index.json b/phases/internal-release/index.json new file mode 100644 index 0000000..290efb0 --- /dev/null +++ b/phases/internal-release/index.json @@ -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" + } + ] +} diff --git a/phases/internal-release/step0.md b/phases/internal-release/step0.md new file mode 100644 index 0000000..2bc7662 --- /dev/null +++ b/phases/internal-release/step0.md @@ -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 파일을 다시 요구하지 마라. 이유: 승인된 검증 계약과 다르다. diff --git a/phases/internal-release/step1.md b/phases/internal-release/step1.md new file mode 100644 index 0000000..b18e74e --- /dev/null +++ b/phases/internal-release/step1.md @@ -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 불일치 위험이 있다. diff --git a/phases/internal-release/step2.md b/phases/internal-release/step2.md new file mode 100644 index 0000000..bef7676 --- /dev/null +++ b/phases/internal-release/step2.md @@ -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만 제공한다. diff --git a/phases/internal-release/step3.md b/phases/internal-release/step3.md new file mode 100644 index 0000000..3ee6315 --- /dev/null +++ b/phases/internal-release/step3.md @@ -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에 묶지 마라. 이유: 머신 성능에 따라 달라진다. +- 결과 정확성 검사를 생략하지 마라. 이유: 빠른 오답은 성능 증거가 아니다. +- 실제 측정 없이 목표 시간을 만들지 마라. 이유: 요구사항에 근거가 없다. diff --git a/phases/internal-release/step4.md b/phases/internal-release/step4.md new file mode 100644 index 0000000..bd799dc --- /dev/null +++ b/phases/internal-release/step4.md @@ -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`를 명시한 경우에만 허용된다. diff --git a/phases/result-contract-completion/index.json b/phases/result-contract-completion/index.json new file mode 100644 index 0000000..5675b2b --- /dev/null +++ b/phases/result-contract-completion/index.json @@ -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" + } + ] +} diff --git a/phases/result-contract-completion/step0.md b/phases/result-contract-completion/step0.md new file mode 100644 index 0000000..e8fcd7c --- /dev/null +++ b/phases/result-contract-completion/step0.md @@ -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 section_strain; + std::array section_force; + double centroid_sigma_xx; + std::vector sigma_xx; +}; +[[nodiscard]] std::vector recover_beam3d2( + const Beam3D2Input&, + std::span element_displacement, + std::span> 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 책임이다. +- 절점별 내력 부호를 임의 절댓값으로 바꾸지 마라. 이유: 평형 검증을 깨뜨린다. diff --git a/phases/result-contract-completion/step1.md b/phases/result-contract-completion/step1.md new file mode 100644 index 0000000..fbae03c --- /dev/null +++ b/phases/result-contract-completion/step1.md @@ -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 end_results; +}; +struct ElementFrame final { + std::vector 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 계약을 재사용해야 한다. diff --git a/phases/result-contract-completion/step2.md b/phases/result-contract-completion/step2.md new file mode 100644 index 0000000..a755378 --- /dev/null +++ b/phases/result-contract-completion/step2.md @@ -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 호환성을 잃는다. diff --git a/phases/results-and-pipeline/index.json b/phases/results-and-pipeline/index.json new file mode 100644 index 0000000..c4ee8f6 --- /dev/null +++ b/phases/results-and-pipeline/index.json @@ -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" + } + ] +} diff --git a/phases/results-and-pipeline/step0.md b/phases/results-and-pipeline/step0.md new file mode 100644 index 0000000..6b0d30f --- /dev/null +++ b/phases/results-and-pipeline/step0.md @@ -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 node_ids; + std::vector> displacement; + std::vector> reaction; +}; +struct ResultFrame final { + double step_time; + NodalFrame nodal; + std::vector diagnostics; +}; +struct ResultStep final { + std::string name; + std::vector frames; +}; +struct ResultDatabase final { + std::string schema_version; + std::vector 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에서만 실체화한다. diff --git a/phases/results-and-pipeline/step1.md b/phases/results-and-pipeline/step1.md new file mode 100644 index 0000000..1f7f6e6 --- /dev/null +++ b/phases/results-and-pipeline/step1.md @@ -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 database; + std::vector diagnostics; +}; +[[nodiscard]] std::vector 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에 빈 미래 분석 결과를 넣지 마라. 이유: 현재 계약만 저장한다. diff --git a/phases/results-and-pipeline/step2.md b/phases/results-and-pipeline/step2.md new file mode 100644 index 0000000..8ade9ee --- /dev/null +++ b/phases/results-and-pipeline/step2.md @@ -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 results; + std::vector 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을 검증해야 한다. diff --git a/phases/results-and-pipeline/step3.md b/phases/results-and-pipeline/step3.md new file mode 100644 index 0000000..356fd97 --- /dev/null +++ b/phases/results-and-pipeline/step3.md @@ -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 --output +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 재사용성을 훼손한다. diff --git a/phases/solver-bootstrap/index.json b/phases/solver-bootstrap/index.json new file mode 100644 index 0000000..dd78e5d --- /dev/null +++ b/phases/solver-bootstrap/index.json @@ -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" + } + ] +} diff --git a/phases/solver-bootstrap/step0.md b/phases/solver-bootstrap/step0.md new file mode 100644 index 0000000..e5a264a --- /dev/null +++ b/phases/solver-bootstrap/step0.md @@ -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/` 아래에만 둔다. +- 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 최소 실체화 원칙을 위반한다. diff --git a/phases/solver-bootstrap/step1.md b/phases/solver-bootstrap/step1.md new file mode 100644 index 0000000..c83ca01 --- /dev/null +++ b/phases/solver-bootstrap/step1.md @@ -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 계약만 다룬다. diff --git a/phases/solver-bootstrap/step2.md b/phases/solver-bootstrap/step2.md new file mode 100644 index 0000000..6f867e3 --- /dev/null +++ b/phases/solver-bootstrap/step2.md @@ -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 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 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` 단계의 책임이다. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5ee6477 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/reference/cantilever beam/Job-1.inp b/reference/cantilever beam/Job-1.inp new file mode 100644 index 0000000..700fab7 --- /dev/null +++ b/reference/cantilever beam/Job-1.inp @@ -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 diff --git a/reference/cantilever beam/cantilever beam displacements.csv b/reference/cantilever beam/cantilever beam displacements.csv new file mode 100644 index 0000000..2eb9c64 --- /dev/null +++ b/reference/cantilever beam/cantilever beam displacements.csv @@ -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 diff --git a/reference/cantilever beam/cantilever beam reactions.csv b/reference/cantilever beam/cantilever beam reactions.csv new file mode 100644 index 0000000..245e7cc --- /dev/null +++ b/reference/cantilever beam/cantilever beam reactions.csv @@ -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 diff --git a/reference/cantilever beam/cantilever beam.inp b/reference/cantilever beam/cantilever beam.inp new file mode 100644 index 0000000..60d62b7 --- /dev/null +++ b/reference/cantilever beam/cantilever beam.inp @@ -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 diff --git a/scripts/execute.py b/scripts/execute.py new file mode 100644 index 0000000..3ca984b --- /dev/null +++ b/scripts/execute.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +""" +Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다. + +Usage: + python scripts/execute.py [--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() diff --git a/scripts/hooks/pre_tool_use.py b/scripts/hooks/pre_tool_use.py new file mode 100644 index 0000000..bca43ba --- /dev/null +++ b/scripts/hooks/pre_tool_use.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Codex PreToolUse 정책: 위험 명령과 테스트 없는 구현 파일 수정을 차단한다.""" + +import json +import re +import sys +from pathlib import Path +from typing import Any + +SCRIPTS_DIR = Path(__file__).resolve().parents[1] +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +from msvc_harness.config import ConfigError, load_config +from msvc_harness.tdd_policy import CPP_SUFFIXES, evaluate_paths + + +DANGEROUS_PATTERNS = ( + re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE), + re.compile(r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b", re.IGNORECASE), + re.compile(r"\brm\s+-rf\b", re.IGNORECASE), + re.compile( + r"\bRemove-Item\b(?=[^\n]*-Recurse\b)(?=[^\n]*-Force\b)", + re.IGNORECASE, + ), + re.compile( + r"\b(?:rmdir|rd)\b(?=[^\n]*/s\b)(?=[^\n]*/q\b)", + re.IGNORECASE, + ), + re.compile(r"\bDROP\s+TABLE\b", re.IGNORECASE), +) +PATCH_PATH = re.compile( + r"^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:) (?P.+?)\s*$", + re.MULTILINE, +) + + +def _tool_input(payload: dict[str, Any]) -> dict[str, Any]: + value = payload.get("tool_input", {}) + return value if isinstance(value, dict) else {} + + +def _candidate_paths(payload: dict[str, Any], root: Path) -> list[Path]: + tool_input = _tool_input(payload) + raw_paths: list[str] = [] + for key in ("path", "file_path"): + value = tool_input.get(key) + if isinstance(value, str) and value.strip(): + raw_paths.append(value.strip()) + + edits = tool_input.get("edits") + if isinstance(edits, list): + for edit in edits: + if not isinstance(edit, dict): + continue + value = edit.get("path") + if isinstance(value, str) and value.strip(): + raw_paths.append(value.strip()) + + for key in ("patch", "input"): + value = tool_input.get(key) + if isinstance(value, str): + raw_paths.extend(match.group("path") for match in PATCH_PATH.finditer(value)) + + paths: list[Path] = [] + for raw_path in raw_paths: + path = Path(raw_path) + paths.append(path.resolve() if path.is_absolute() else (root / path).resolve()) + return list(dict.fromkeys(paths)) + + +def evaluate(payload: dict[str, Any], root: Path) -> str | None: + """Return a blocking reason, or None when the tool call is allowed.""" + tool_name = str(payload.get("tool_name", "")) + tool_input = _tool_input(payload) + + if tool_name in {"Bash", "shell_command", "PowerShell"}: + command = tool_input.get("command", "") + if isinstance(command, str) and any( + pattern.search(command) for pattern in DANGEROUS_PATTERNS + ): + return "위험한 명령어가 감지되어 실행을 차단했습니다." + + if tool_name not in {"apply_patch", "Edit", "MultiEdit", "Write"}: + return None + + paths = _candidate_paths(payload, root) + try: + config = load_config(root) + except ConfigError as exc: + if any(path.suffix.lower() in CPP_SUFFIXES for path in paths): + return f"TDD GUARD: .harness/config.json must be repaired: {exc}" + return None + return evaluate_paths(paths, root, config.tdd) + + +def main() -> int: + try: + payload = json.load(sys.stdin) + if not isinstance(payload, dict): + raise TypeError("hook input must be a JSON object") + cwd = payload.get("cwd") + if cwd is not None and not isinstance(cwd, str): + raise TypeError("hook cwd must be a string") + except (json.JSONDecodeError, TypeError) as exc: + print(f"TDD GUARD: hook 입력을 해석하지 못해 검사를 건너뜁니다: {exc}", file=sys.stderr) + return 0 + + root = Path(payload.get("cwd") or Path.cwd()).resolve() + reason = evaluate(payload, root) + if reason: + print(reason, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hooks/stop_validation.py b/scripts/hooks/stop_validation.py new file mode 100644 index 0000000..5c58c8c --- /dev/null +++ b/scripts/hooks/stop_validation.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Codex Stop hook: validate discovered C/C++ projects with MSVC.""" + +import json +import os +import subprocess +import sys +import time +from pathlib import Path + + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) + +from msvc_harness.adapters.base import AdapterError +from msvc_harness.adapters.cmake import CMakeAdapter +from msvc_harness.adapters.msbuild import MSBuildAdapter +from msvc_harness.config import ConfigError, load_config +from msvc_harness.discovery import DiscoveryError, discover_project +from msvc_harness.models import ProjectKind +from msvc_harness.process import ValidationFailure, execute_plan +from msvc_harness.toolchain import ToolchainError, discover_toolchain + + +REENTRY_ENV = "CODEX_STOP_VALIDATION_ACTIVE" +TOTAL_TIMEOUT_SECONDS = 1800 + + +def _remaining(deadline, clock, stage): + remaining = deadline - clock() + if remaining <= 0: + raise ValidationFailure(f"{stage} timed out before it could start") + return remaining + + +def _project_root( + cwd: Path, + *, + deadline: float, + run=subprocess.run, + clock=time.monotonic, +) -> Path: + argv = ["git", "rev-parse", "--show-toplevel"] + resolved_cwd = cwd.resolve() + try: + result = run( + argv, + cwd=resolved_cwd, + capture_output=True, + text=True, + shell=False, + timeout=_remaining(deadline, clock, "repository discovery"), + ) + except subprocess.TimeoutExpired as exc: + raise ValidationFailure( + f"repository discovery timed out; argv={argv!r}; " + f"cwd={str(resolved_cwd)!r}" + ) from exc + if result.returncode == 0 and result.stdout.strip(): + return Path(result.stdout.strip()).resolve() + return resolved_cwd + + +def run_validations( + root: Path, + *, + deadline: float | None = None, + clock=time.monotonic, +) -> tuple[bool, str]: + """Build and test a discovered C/C++ project, when one exists.""" + if deadline is None: + deadline = clock() + TOTAL_TIMEOUT_SECONDS + try: + config = load_config(root) + discovery = discover_project(root, config) + if discovery.selection is None: + return True, "" + selection = discovery.selection + tools = discover_toolchain( + selection.kind, + deadline=deadline, + clock=clock, + ) + adapter = ( + CMakeAdapter() + if selection.kind is ProjectKind.CMAKE + else MSBuildAdapter() + ) + plan = adapter.create_plan(root, selection, config, tools) + child_env = os.environ.copy() + child_env[REENTRY_ENV] = "1" + execute_plan( + plan, + root, + env=child_env, + deadline=deadline, + clock=clock, + ) + return True, "" + except ( + ConfigError, + DiscoveryError, + ToolchainError, + AdapterError, + ValidationFailure, + OSError, + ) as exc: + return False, str(exc) + + +def _emit_stop_response(message: str) -> None: + print( + json.dumps( + { + "continue": False, + "stopReason": message, + "systemMessage": message, + }, + ensure_ascii=False, + ) + ) + + +def main(*, run=subprocess.run, clock=time.monotonic) -> int: + if os.environ.get(REENTRY_ENV) == "1": + return 0 + + deadline = clock() + TOTAL_TIMEOUT_SECONDS + try: + root = _project_root( + Path.cwd(), + deadline=deadline, + run=run, + clock=clock, + ) + ok, message = run_validations(root, deadline=deadline, clock=clock) + except (OSError, ValidationFailure) as exc: + _emit_stop_response(f"validation hook failed: {exc}") + return 0 + + if ok: + return 0 + _emit_stop_response(message) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/msvc_harness/__init__.py b/scripts/msvc_harness/__init__.py new file mode 100644 index 0000000..c59cfff --- /dev/null +++ b/scripts/msvc_harness/__init__.py @@ -0,0 +1,3 @@ +from .config import ConfigError, load_config + +__all__ = ["ConfigError", "load_config"] diff --git a/scripts/msvc_harness/adapters/__init__.py b/scripts/msvc_harness/adapters/__init__.py new file mode 100644 index 0000000..c343612 --- /dev/null +++ b/scripts/msvc_harness/adapters/__init__.py @@ -0,0 +1,5 @@ +from .base import AdapterError, ValidationAdapter +from .cmake import CMakeAdapter +from .msbuild import MSBuildAdapter + +__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"] diff --git a/scripts/msvc_harness/adapters/base.py b/scripts/msvc_harness/adapters/base.py new file mode 100644 index 0000000..0c97c4e --- /dev/null +++ b/scripts/msvc_harness/adapters/base.py @@ -0,0 +1,19 @@ +from pathlib import Path +from typing import Protocol + +from ..models import HarnessConfig, ProjectSelection, Toolchain, ValidationPlan + + +class AdapterError(RuntimeError): + pass + + +class ValidationAdapter(Protocol): + def create_plan( + self, + root: Path, + selection: ProjectSelection, + config: HarnessConfig, + tools: Toolchain, + ) -> ValidationPlan: + raise NotImplementedError diff --git a/scripts/msvc_harness/adapters/cmake.py b/scripts/msvc_harness/adapters/cmake.py new file mode 100644 index 0000000..f7d449d --- /dev/null +++ b/scripts/msvc_harness/adapters/cmake.py @@ -0,0 +1,99 @@ +from pathlib import Path + +from ..models import ( + CommandSpec, + HarnessConfig, + ProjectKind, + ProjectSelection, + ResultCheck, + ResultCheckKind, + Toolchain, + ValidationPlan, + ValidationStep, +) +from .base import AdapterError + + +class CMakeAdapter: + def create_plan( + self, + root: Path, + selection: ProjectSelection, + config: HarnessConfig, + tools: Toolchain, + ) -> ValidationPlan: + if selection.kind is not ProjectKind.CMAKE: + raise AdapterError("CMakeAdapter requires a CMake project selection") + if tools.cmake is None: + raise AdapterError("CMake tool is required") + if tools.ctest is None: + raise AdapterError("CTest tool is required") + + if config.cmake.configure_preset is None: + command_cwd = root + binary = root / ".harness/build" + configure = ( + str(tools.cmake), + "-S", + str(config.cmake.source_dir), + "-B", + str(binary), + "-A", + "x64", + ) + build = (str(tools.cmake), "--build", str(binary), "--config", "Debug") + discover = ( + str(tools.ctest), + "--test-dir", + str(binary), + "-C", + "Debug", + "--show-only=json-v1", + ) + test = ( + str(tools.ctest), + "--test-dir", + str(binary), + "-C", + "Debug", + "--output-on-failure", + ) + else: + command_cwd = config.cmake.source_dir + binary = config.cmake.binary_dir + if ( + binary is None + or config.cmake.build_preset is None + or config.cmake.test_preset is None + ): + raise AdapterError("CMake presets require binary, build, and test settings") + configure = (str(tools.cmake), "--preset", config.cmake.configure_preset) + build = (str(tools.cmake), "--build", "--preset", config.cmake.build_preset) + discover = ( + str(tools.ctest), + "--preset", + config.cmake.test_preset, + "--show-only=json-v1", + ) + test = ( + str(tools.ctest), + "--preset", + config.cmake.test_preset, + "--output-on-failure", + ) + + return ValidationPlan( + ProjectKind.CMAKE, + ( + ValidationStep( + CommandSpec(configure, command_cwd, "configure"), + (ResultCheck(ResultCheckKind.CMAKE_COMPILER_IS_MSVC, binary),), + ), + ValidationStep(CommandSpec(build, command_cwd, "build")), + ValidationStep( + CommandSpec(discover, command_cwd, "test-discovery"), + (ResultCheck(ResultCheckKind.CTEST_HAS_TESTS),), + ), + ValidationStep(CommandSpec(test, command_cwd, "test")), + ), + ) diff --git a/scripts/msvc_harness/adapters/msbuild.py b/scripts/msvc_harness/adapters/msbuild.py new file mode 100644 index 0000000..00c95e0 --- /dev/null +++ b/scripts/msvc_harness/adapters/msbuild.py @@ -0,0 +1,60 @@ +from pathlib import Path + +from ..models import ( + CommandSpec, + HarnessConfig, + ProjectKind, + ProjectSelection, + Toolchain, + ValidationPlan, + ValidationStep, +) +from .base import AdapterError + + +def _test_argv(root: Path, command: tuple[str, ...]) -> tuple[str, ...]: + if "/" not in command[0] and "\\" not in command[0]: + return command + first = Path(command[0].replace("\\", "/")) + executable = first if first.is_absolute() else (root / first).resolve() + try: + executable.relative_to(root) + except ValueError as exc: + raise AdapterError( + "msbuild.testCommand resolves outside the repository" + ) from exc + return (str(executable), *command[1:]) + + +class MSBuildAdapter: + def create_plan( + self, + root: Path, + selection: ProjectSelection, + config: HarnessConfig, + tools: Toolchain, + ) -> ValidationPlan: + if selection.kind is not ProjectKind.MSBUILD: + raise AdapterError("MSBuildAdapter received a non-MSBuild project") + if not config.msbuild.test_command: + raise AdapterError( + "msbuild.testCommand is required for .sln/.vcxproj validation" + ) + build = ValidationStep( + CommandSpec( + ( + str(tools.msbuild), + str(selection.project_file), + "/m", + "/nologo", + f"/p:Configuration={config.msbuild.configuration}", + f"/p:Platform={config.msbuild.platform}", + ), + root, + "build", + ) + ) + test = ValidationStep( + CommandSpec(_test_argv(root, config.msbuild.test_command), root, "test") + ) + return ValidationPlan(ProjectKind.MSBUILD, (build, test)) diff --git a/scripts/msvc_harness/config.py b/scripts/msvc_harness/config.py new file mode 100644 index 0000000..dd9ef43 --- /dev/null +++ b/scripts/msvc_harness/config.py @@ -0,0 +1,146 @@ +import json +from pathlib import Path +from typing import Any + +from .models import CMakeConfig, HarnessConfig, MSBuildConfig, TDDConfig + +DEFAULT_TEST_PATTERNS = ( + "{stem}_test.cpp", + "{stem}_tests.cpp", + "test_{stem}.cpp", + "{stem}.test.cpp", +) + + +class ConfigError(ValueError): + pass + + +def _reject_unknown(data: dict[str, Any], allowed: set[str], label: str) -> None: + unknown = sorted(set(data) - allowed) + if unknown: + raise ConfigError(f"{label} contains unexpected field: {unknown[0]}") + + +def _repo_path(root: Path, raw: str, label: str) -> Path: + candidate = Path(raw) + if candidate.is_absolute(): + raise ConfigError(f"{label} must be repository-relative") + resolved = (root / candidate).resolve() + try: + resolved.relative_to(root) + except ValueError as exc: + raise ConfigError(f"{label} resolves outside the repository") from exc + return resolved + + +def _mapping(value: Any, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise ConfigError(f"{label} must be an object") + return value + + +def _nonempty_string(value: Any, label: str) -> str: + if not isinstance(value, str) or not value: + raise ConfigError(f"{label} must be a non-empty string") + return value + + +def _string_array(value: Any, label: str) -> tuple[str, ...]: + if not isinstance(value, list): + raise ConfigError(f"{label} must be an array") + if not value or any(not isinstance(item, str) or not item for item in value): + raise ConfigError(f"{label} must be a non-empty string array") + return tuple(value) + + +def load_config(root: Path) -> HarnessConfig: + root = root.resolve() + config_path = root / ".harness" / "config.json" + if config_path.is_file(): + try: + data = json.loads(config_path.read_text(encoding="utf-8")) + except UnicodeDecodeError as exc: + raise ConfigError( + f"{config_path}: configuration must be valid UTF-8: {exc}" + ) from exc + except json.JSONDecodeError as exc: + raise ConfigError(f"{config_path}: {exc}") from exc + else: + data = {"version": 1} + + data = _mapping(data, "config") + _reject_unknown(data, {"version", "projectType", "cmake", "msbuild", "tdd"}, "config") + + if data.get("version") != 1 or isinstance(data.get("version"), bool): + raise ConfigError("version must be 1") + project_type = data.get("projectType", "auto") + if project_type not in {"auto", "cmake", "msbuild"}: + raise ConfigError("projectType must be auto, cmake, or msbuild") + + cmake_data = _mapping(data.get("cmake", {}), "cmake") + _reject_unknown( + cmake_data, + {"sourceDir", "binaryDir", "configurePreset", "buildPreset", "testPreset"}, + "cmake", + ) + source_dir = _repo_path( + root, _nonempty_string(cmake_data.get("sourceDir", "."), "cmake.sourceDir"), "cmake.sourceDir" + ) + binary_dir = None + if "binaryDir" in cmake_data: + binary_dir = _repo_path( + root, _nonempty_string(cmake_data["binaryDir"], "cmake.binaryDir"), "cmake.binaryDir" + ) + preset_keys = ("configurePreset", "buildPreset", "testPreset", "binaryDir") + if any(key in cmake_data for key in preset_keys) and not all(key in cmake_data for key in preset_keys): + raise ConfigError("cmake configurePreset, buildPreset, testPreset, and binaryDir must be specified together") + configure_preset = ( + _nonempty_string(cmake_data["configurePreset"], "cmake.configurePreset") + if "configurePreset" in cmake_data + else None + ) + build_preset = ( + _nonempty_string(cmake_data["buildPreset"], "cmake.buildPreset") + if "buildPreset" in cmake_data + else None + ) + test_preset = ( + _nonempty_string(cmake_data["testPreset"], "cmake.testPreset") + if "testPreset" in cmake_data + else None + ) + + msbuild_data = _mapping(data.get("msbuild", {}), "msbuild") + _reject_unknown(msbuild_data, {"solution", "configuration", "platform", "testCommand"}, "msbuild") + solution = None + if "solution" in msbuild_data: + solution = _repo_path( + root, _nonempty_string(msbuild_data["solution"], "msbuild.solution"), "msbuild.solution" + ) + configuration = _nonempty_string(msbuild_data.get("configuration", "Debug"), "msbuild.configuration") + platform = _nonempty_string(msbuild_data.get("platform", "x64"), "msbuild.platform") + test_command = ( + _string_array(msbuild_data["testCommand"], "msbuild.testCommand") + if "testCommand" in msbuild_data + else None + ) + + tdd_data = _mapping(data.get("tdd", {}), "tdd") + _reject_unknown(tdd_data, {"testRoots", "testPatterns", "exclude"}, "tdd") + raw_test_roots = _string_array(tdd_data["testRoots"], "tdd.testRoots") if "testRoots" in tdd_data else ("tests", "test") + test_roots = tuple(_repo_path(root, item, "tdd.testRoots") for item in raw_test_roots) + test_patterns = _string_array(tdd_data["testPatterns"], "tdd.testPatterns") if "testPatterns" in tdd_data else DEFAULT_TEST_PATTERNS + if any("{stem}" not in pattern for pattern in test_patterns): + raise ConfigError("every tdd.testPatterns entry must contain {stem}") + exclude = _string_array(tdd_data["exclude"], "tdd.exclude") if "exclude" in tdd_data else () + for item in exclude: + _repo_path(root, item, "tdd.exclude") + + return HarnessConfig( + version=1, + project_type=project_type, + cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset), + msbuild=MSBuildConfig(solution, configuration, platform, test_command), + tdd=TDDConfig(test_roots, test_patterns, exclude), + ) diff --git a/scripts/msvc_harness/discovery.py b/scripts/msvc_harness/discovery.py new file mode 100644 index 0000000..282c69d --- /dev/null +++ b/scripts/msvc_harness/discovery.py @@ -0,0 +1,77 @@ +from pathlib import Path + +from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection +from .tdd_policy import find_cpp_files + + +class DiscoveryError(RuntimeError): + pass + + +def _cmake_entry(source: Path) -> Path | None: + for name in ("CMakePresets.json", "CMakeUserPresets.json", "CMakeLists.txt"): + candidate = source / name + if candidate.is_file(): + return candidate + return None + + +def _single_msbuild(root: Path, explicit: Path | None) -> Path: + if explicit is not None: + if explicit.is_file() and explicit.suffix.lower() in {".sln", ".vcxproj"}: + return explicit + raise DiscoveryError(f"configured MSBuild project is invalid: {explicit}") + solutions = sorted(root.glob("*.sln")) + if len(solutions) > 1: + raise DiscoveryError("multiple .sln files found; set msbuild.solution") + if solutions: + return solutions[0] + projects = sorted(root.glob("*.vcxproj")) + if len(projects) > 1: + raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution") + if projects: + return projects[0] + raise DiscoveryError("no root .sln or .vcxproj was found") + + +def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult: + root = root.resolve() + if config.project_type == "cmake": + entry = _cmake_entry(config.cmake.source_dir) + if entry is None: + raise DiscoveryError( + f"no CMake project found in {config.cmake.source_dir}" + ) + return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry)) + + if config.project_type == "msbuild": + project = _single_msbuild(root, config.msbuild.solution) + return DiscoveryResult(ProjectSelection(ProjectKind.MSBUILD, project)) + + entry = _cmake_entry(root) + if entry is not None: + return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry)) + + solutions = sorted(root.glob("*.sln")) + if len(solutions) > 1: + raise DiscoveryError("multiple .sln files found; set msbuild.solution") + if solutions: + return DiscoveryResult( + ProjectSelection(ProjectKind.MSBUILD, solutions[0]) + ) + + projects = sorted(root.glob("*.vcxproj")) + if len(projects) > 1: + raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution") + if projects: + return DiscoveryResult( + ProjectSelection(ProjectKind.MSBUILD, projects[0]) + ) + + cpp_files = find_cpp_files(root) + if cpp_files: + raise DiscoveryError( + "C/C++ files exist but no CMakeLists.txt, preset, .sln, or .vcxproj " + "was found; configure projectType and project path" + ) + return DiscoveryResult(None, ()) diff --git a/scripts/msvc_harness/models.py b/scripts/msvc_harness/models.py new file mode 100644 index 0000000..1c03085 --- /dev/null +++ b/scripts/msvc_harness/models.py @@ -0,0 +1,100 @@ +from dataclasses import dataclass +from enum import Enum +from pathlib import Path + + +class ProjectKind(Enum): + CMAKE = "cmake" + MSBUILD = "msbuild" + + +class ResultCheckKind(Enum): + CMAKE_COMPILER_IS_MSVC = "cmake_compiler_is_msvc" + CTEST_HAS_TESTS = "ctest_has_tests" + + +@dataclass(frozen=True) +class CMakeConfig: + source_dir: Path + binary_dir: Path | None = None + configure_preset: str | None = None + build_preset: str | None = None + test_preset: str | None = None + + +@dataclass(frozen=True) +class MSBuildConfig: + solution: Path | None = None + configuration: str = "Debug" + platform: str = "x64" + test_command: tuple[str, ...] | None = None + + +@dataclass(frozen=True) +class TDDConfig: + test_roots: tuple[Path, ...] + test_patterns: tuple[str, ...] + exclude: tuple[str, ...] = () + + +@dataclass(frozen=True) +class HarnessConfig: + version: int + project_type: str + cmake: CMakeConfig + msbuild: MSBuildConfig + tdd: TDDConfig + + +@dataclass(frozen=True) +class ProjectSelection: + kind: ProjectKind + project_file: Path + + +@dataclass(frozen=True) +class DiscoveryResult: + selection: ProjectSelection | None + cpp_files: tuple[Path, ...] = () + + +@dataclass(frozen=True) +class Toolchain: + installation: Path + msbuild: Path + cmake: Path | None = None + ctest: Path | None = None + + +@dataclass(frozen=True) +class CommandSpec: + argv: tuple[str, ...] + cwd: Path + stage: str + timeout_seconds: int = 1800 + + +@dataclass(frozen=True) +class ResultCheck: + kind: ResultCheckKind + path: Path | None = None + + +@dataclass(frozen=True) +class ValidationStep: + command: CommandSpec + checks: tuple[ResultCheck, ...] = () + + +@dataclass(frozen=True) +class ValidationPlan: + project_kind: ProjectKind + steps: tuple[ValidationStep, ...] + + +@dataclass(frozen=True) +class CommandResult: + command: CommandSpec + returncode: int + stdout: str + stderr: str diff --git a/scripts/msvc_harness/process.py b/scripts/msvc_harness/process.py new file mode 100644 index 0000000..0911b13 --- /dev/null +++ b/scripts/msvc_harness/process.py @@ -0,0 +1,140 @@ +import json +import locale +import re +import subprocess +import time + +from .models import CommandResult, ResultCheckKind + +DIAGNOSTIC_LIMIT = 8000 + + +class ValidationFailure(RuntimeError): + pass + + +def _decode(value): + if value is None: + return "" + if isinstance(value, str): + return value + for encoding in ("utf-8", locale.getpreferredencoding(False)): + try: + return value.decode(encoding) + except (UnicodeDecodeError, LookupError): + continue + return value.decode("utf-8", errors="replace") + + +def _check_result(check, result): + if check.kind is ResultCheckKind.CMAKE_COMPILER_IS_MSVC: + if check.path is None: + raise ValidationFailure("MSVC check is missing binaryDir") + files = sorted(check.path.glob("CMakeFiles/*/CMakeCXXCompiler.cmake")) + if not files: + raise ValidationFailure("CMake did not generate compiler metadata") + text = files[-1].read_text(encoding="utf-8", errors="replace") + if not re.search( + r'set\s*\(\s*CMAKE_CXX_COMPILER_ID\s+"MSVC"\s*\)', text + ): + raise ValidationFailure("CMake selected a compiler other than MSVC") + elif check.kind is ResultCheckKind.CTEST_HAS_TESTS: + try: + payload = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise ValidationFailure("CTest discovery did not return JSON") from exc + if not isinstance(payload, dict): + raise ValidationFailure("CTest discovery JSON must be an object") + if not isinstance(payload.get("tests"), list) or not payload["tests"]: + raise ValidationFailure("CTest discovered no tests") + + +def _diagnostic(stdout, stderr): + diagnostic = (stdout + "\n" + stderr).rstrip()[-DIAGNOSTIC_LIMIT:] + return diagnostic.replace("\r\n", " | ").replace("\n", " | ") + + +def _command_context(argv, cwd): + return f"argv={list(argv)!r}; cwd={str(cwd)!r}" + + +def execute_plan( + plan, + root, + *, + env=None, + total_timeout_seconds=1800, + deadline=None, + run=subprocess.run, + clock=time.monotonic, +): + root = root.resolve() + if deadline is None: + deadline = clock() + total_timeout_seconds + results = [] + + for step in plan.steps: + cwd = step.command.cwd.resolve() + context = _command_context(step.command.argv, cwd) + try: + cwd.relative_to(root) + except ValueError as exc: + raise ValidationFailure( + f"{step.command.stage} working directory is outside the repository; " + f"{context}" + ) from exc + + remaining = deadline - clock() + if remaining <= 0: + raise ValidationFailure( + f"{step.command.stage} timed out before it could start; {context}" + ) + + timeout = min(step.command.timeout_seconds, remaining) + try: + completed = run( + list(step.command.argv), + cwd=cwd, + env=env, + capture_output=True, + shell=False, + timeout=timeout, + ) + except subprocess.TimeoutExpired as exc: + stdout = _decode(exc.output) + stderr = _decode(exc.stderr) + diagnostic = _diagnostic(stdout, stderr) + message = ( + f"{step.command.stage} timed out after {timeout} seconds; {context}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ValidationFailure(message) from exc + + stdout = _decode(completed.stdout) + stderr = _decode(completed.stderr) + result = CommandResult(step.command, completed.returncode, stdout, stderr) + if completed.returncode != 0: + diagnostic = _diagnostic(stdout, stderr) + message = ( + f"{step.command.stage} failed with exit code {completed.returncode}; " + f"{context}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ValidationFailure(message) + + try: + for check in step.checks: + _check_result(check, result) + except ValidationFailure as exc: + diagnostic = _diagnostic(stdout, stderr) + message = ( + f"{step.command.stage} result check failed: {exc}; {context}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ValidationFailure(message) from exc + results.append(result) + + return tuple(results) diff --git a/scripts/msvc_harness/tdd_policy.py b/scripts/msvc_harness/tdd_policy.py new file mode 100644 index 0000000..b5f5b8c --- /dev/null +++ b/scripts/msvc_harness/tdd_policy.py @@ -0,0 +1,88 @@ +from fnmatch import fnmatch +from pathlib import Path + +CPP_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx"}) +DEFAULT_EXCLUDES = ( + ".harness/build/**", + "build/**", + "out/**", + "cmake-build-*/**", + "third_party/**", + "external/**", + "vendor/**", + "generated/**", +) + + +def is_excluded_path(path: Path, root: Path, extra: tuple[str, ...] = ()) -> bool: + try: + relative = path.resolve().relative_to(root.resolve()).as_posix() + except ValueError: + return False + return any(fnmatch(relative, pattern) for pattern in DEFAULT_EXCLUDES + extra) + + +def find_cpp_files( + root: Path, + extra_excludes: tuple[str, ...] = (), +) -> tuple[Path, ...]: + found = ( + path + for path in root.resolve().rglob("*") + if path.is_file() + and path.suffix.lower() in CPP_SUFFIXES + and not is_excluded_path(path, root, extra_excludes) + ) + return tuple(sorted(found)) + + +def is_test_file(path: Path, root: Path, config) -> bool: + resolved = path.resolve() + if any(root == resolved or root in resolved.parents for root in config.test_roots): + return True + relative = resolved.relative_to(root.resolve()) + if {"test", "tests"} & {part.lower() for part in relative.parts}: + return True + stem = resolved.stem.lower() + return stem.startswith("test_") or stem.endswith(("_test", "_tests", ".test")) + + +def matching_test_exists(path: Path, config) -> bool: + names = tuple(pattern.format(stem=path.stem) for pattern in config.test_patterns) + roots = ( + *config.test_roots, + path.parent / "tests", + path.parent / "test", + ) + for root in roots: + if not root.is_dir(): + continue + for name in names: + if any(candidate.is_file() for candidate in root.rglob(name)): + return True + return False + + +def evaluate_paths(paths, root: Path, config) -> str | None: + root = root.resolve() + for raw in paths: + path = raw.resolve() + try: + path.relative_to(root) + except ValueError: + return f"TDD GUARD: '{path}' is outside the repository" + if path.suffix.lower() not in CPP_SUFFIXES: + continue + if ( + path.name.lower() == "main.cpp" + or is_excluded_path(path, root, config.exclude) + or is_test_file(path, root, config) + ): + continue + if not matching_test_exists(path, config): + expected = config.test_patterns[0].format(stem=path.stem) + return ( + f"TDD GUARD: '{path.name}' requires an existing test such as " + f"'{expected}'. Add the test in a configured test root first." + ) + return None diff --git a/scripts/msvc_harness/toolchain.py b/scripts/msvc_harness/toolchain.py new file mode 100644 index 0000000..b1d63d0 --- /dev/null +++ b/scripts/msvc_harness/toolchain.py @@ -0,0 +1,169 @@ +import os +import shutil +import subprocess +import time +from pathlib import Path + +from .models import ProjectKind, Toolchain + +VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" +VS_CMAKE_RELATIVE = Path( + "Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin" +) + + +class ToolchainError(RuntimeError): + pass + + +def _decode(value): + if isinstance(value, str): + return value + return (value or b"").decode("utf-8", errors="replace") + + +def _first_path(result, label, argv, cwd): + text = _decode(result.stdout).strip() + if result.returncode != 0: + diagnostic = ( + _decode(result.stdout) + "\n" + _decode(result.stderr) + ).rstrip()[-8000:] + diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ") + message = ( + f"{label} probe failed with exit code {result.returncode}; " + f"argv={argv!r}; cwd={str(cwd)!r}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ToolchainError( + f"{message}; install Visual Studio Desktop development with C++" + ) + if not text: + raise ToolchainError( + f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; " + "install Visual Studio Desktop development with C++" + ) + path = Path(text.splitlines()[0]).resolve() + if not path.exists(): + diagnostic = ( + _decode(result.stdout) + "\n" + _decode(result.stderr) + ).rstrip()[-8000:] + diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ") + message = ( + f"{label} probe returned nonexistent path with exit code 0; " + f"argv={argv!r}; cwd={str(cwd)!r}" + ) + if diagnostic: + message = f"{message}; output tail: {diagnostic}" + raise ToolchainError( + f"{message}; install or repair Visual Studio Desktop development with C++" + ) + return path + + +def _require_path(path, label): + if path is None or not path.exists(): + raise ToolchainError(f"{label} not found") + return path.resolve() + + +def _remaining(deadline, clock, label): + if deadline is None: + return None + remaining = deadline - clock() + if remaining <= 0: + raise ToolchainError(f"{label} probe timed out before it could start") + return remaining + + +def _probe(run, argv, label, *, deadline, clock): + cwd = Path.cwd().resolve() + kwargs = { + "cwd": cwd, + "capture_output": True, + "check": False, + "shell": False, + } + remaining = _remaining(deadline, clock, label) + if remaining is not None: + kwargs["timeout"] = remaining + try: + return run(argv, **kwargs) + except subprocess.TimeoutExpired as exc: + raise ToolchainError( + f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; " + "install or repair Visual Studio Desktop development with C++" + ) from exc + + +def discover_toolchain( + kind: ProjectKind, + *, + env=None, + which=shutil.which, + run=subprocess.run, + deadline=None, + clock=time.monotonic, +) -> Toolchain: + environment = os.environ if env is None else env + vswhere_path = which("vswhere.exe") + if vswhere_path is None: + program_files = environment.get("ProgramFiles(x86)") + if program_files: + candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe" + vswhere_path = str(candidate) if candidate.exists() else None + vswhere = _require_path( + Path(vswhere_path) if vswhere_path else None, + "vswhere.exe; install Visual Studio Desktop development with C++", + ) + + install_argv = [ + str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD, + "-property", "installationPath", + ] + install_result = _probe( + run, + install_argv, + "Visual Studio installation", + deadline=deadline, + clock=clock, + ) + probe_cwd = Path.cwd().resolve() + installation = _require_path( + _first_path( + install_result, + "Visual Studio installation", + install_argv, + probe_cwd, + ), + "Visual Studio installation", + ) + msbuild_argv = [ + str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD, + "-find", r"MSBuild\**\Bin\MSBuild.exe", + ] + msbuild_result = _probe( + run, + msbuild_argv, + "MSBuild", + deadline=deadline, + clock=clock, + ) + msbuild = _require_path( + _first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd), + "MSBuild.exe", + ) + + if kind is not ProjectKind.CMAKE: + return Toolchain(installation=installation, msbuild=msbuild) + + bundled_cmake_dir = installation / VS_CMAKE_RELATIVE + cmake = _require_path( + Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe", + "cmake.exe", + ) + ctest = _require_path( + Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe", + "ctest.exe", + ) + return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest)